pkgsrc/www/php-nextcloud などが特定条件下で segmentation fault になる件

条件が揃うと、nextcloud の occ status のような単純な処理でも segmentation fault が発生する。

根が深かったのでメモっておく。

概要
php で mysql と curl の extension が両方あり、mysql 5.7 以降と openssl 1.0系(以前)を組み合わせていると不具合が発生する(場合がある)。

pkgsrc(2019年2月頃~)の mysql のデフォルトは 5.7、NetBSD 8.[01] の openssl は 1.0.2k、なので引っ掛かりやすい(はず)。
(NetBSD 9.0 からは openssl 1.1系になるはず……)

原因
php-pdo_mysql の dbh_constructor ? が呼ばれた際、libmysqlclient が openssl にコールバックを設定するが、終了時にそれを解除しないまま libmysqlclient がメモリ上から退場してしまう。
さらに、php の zend.c の php_module_shutdown () から始まる終了処理で、php-curl から openssl の ENGINE_cleanup () を呼んだ際、その(既にメモリ上にない)コールバックを呼ぼうとしてしまう。

回避方法
いずれかで回避できるはず。

(1) mysql 5.6 を使う

(2) curl.so extension の読み込みを pdo_mysql.so より後にする
後に読み込まれたものが先に終了するので、こうすれば libmysqlclient がいるうちにコールバックが呼ばれる。
pkgsrc の場合は /usr/pkg/etc/php.d/*.ini で extension を呼んでると思うので、

# cd /usr/pkg/etc/php.d
# mv curl.ini zzcurl.ini
# echo "; DO NOT LOAD curl.so HERE (see zzcurl.ini)" > curl.ini

とでもするとか。
php.ini に extension=… を列挙する方式なら、curl.so を下に持っていけばよい。

(もっとマシな方法は、思いついたら書く。)

詳細と確認方法
以下、NetBSD 8.1_STABLE, mysql-client-5.7.26nb2 などで実施したメモ。
追試する場合、環境ぶっ壊すかもしれないのでサンドボックスなりなんなりで。

NetBSD のシステムの openssl(というか libcrypto)をデバッグシンボルつきにする。

$ cd /usr/src/lib/libcrypt
$ make USETOOLS=no cleandir depend libcrypt.so
$ cd /usr/src/crypto/external/bsd/openssl/lib/libcrypto
$ make USETOOLS=no DBG="-g -Og" cleandir dependall
$ sudo make USETOOLS=no install

libcrypto.so が libcrypt.so 使うようなので先に作る。libcrypt.so の install はしなくて良い。
crypto と crypt が紛らわしい。

pkgsrc/www/php-nextcloud (お好みで php-owncloud)とその前提も CFLAGS="-g -Og" とかでコンパイルしてインストールし、初期設定をしておく。

www(apache のユーザ)のログインシェルを /sbin/nologin から /bin/sh にでもしておく。
(occ がユーザチェックをするので他のユーザでは動かないのと、nologin のままだと gdb が動かないので。)

設定が終わったら、デバッガの中でブレークポイントを設定の上、nextcloud の occ status を実行してみる。

$ cd /usr/pkg/share/nextcloud
$ sudo -u www gdb php
GNU gdb (GDB) 7.12
...
Reading symbols from php...done.
(gdb) break CRYPTO_set_locking_callback
Breakpoint 1 at 0x44ed20
(gdb) run occ status
Starting program: /usr/pkg/bin/php occ status

Breakpoint 1, CRYPTO_set_locking_callback (
    func=0x7f7d0ac5a12a <openssl_lock_function>)
    at /usr/src/crypto/external/bsd/openssl/dist/crypto/cryptlib.c:407
407     {

ブレークポイントに設定した CRYPTO_set_locking_callback () が引数 func=0x7f7d0ac5a12a <openssl_lock_function> で実行されたということ。
0x7f7d0ac5a12a というアドレスは毎回違うので、あくまで一例ということで。)

gdb の bt コマンドを使えば、呼び出し元が mysql の vio/viosslfactories.cssl_start () あたりなのもわかる。
(長いのでアドレスや引数やその他諸々省略。)

#0  CRYPTO_set_locking_callback at openssl/dist/crypto/cryptlib.c:407
#1  set_lock_callback_functions at mysql-5.7.26/vio/viosslfactories.c:390
#2  init_lock_callback_functions at mysql-5.7.26/vio/viosslfactories.c:399
#3  ssl_start at mysql-5.7.26/vio/viosslfactories.c:449
#4  mysql_server_init at mysql-5.7.26/libmysql/libmysql.c:120
#5  mysql_init at mysql-5.7.26/sql-common/client.c:2471
#6  pdo_mysql_handle_factory at php-7.2.19/ext/pdo_mysql/mysql_driver.c:630
#7  zim_PDO_dbh_constructor at php-7.2.19/ext/pdo/pdo_dbh.c:358
#8  ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER at php-7.2.19/Zend/zend_vm_execute.h:907
#9  execute_ex at php-7.2.19/Zend/zend_vm_execute.h:59765
#10 zend_call_function at php-7.2.19/Zend/zend_execute_API.c:820
#11 zend_call_method at php-7.2.19/Zend/zend_interfaces.c:100
#12 zend_std_read_dimension at php-7.2.19/Zend/zend_object_handlers.c:805
#13 zend_fetch_dimension_address_read at php-7.2.19/Zend/zend_execute.c:1826
#14 zend_fetch_dimension_address_read_R_slow at php-7.2.19/Zend/zend_execute.c:1855
#15 ZEND_FETCH_DIM_R_SPEC_TMPVAR_CV_HANDLER at php-7.2.19/Zend/zend_vm_execute.h:51880
#16 execute_ex at php-7.2.19/Zend/zend_vm_execute.h:63410
...
#50 ZEND_FETCH_DIM_R_SPEC_TMPVAR_CV_HANDLER at php-7.2.19/Zend/zend_vm_execute.h:51880
#51 execute_ex at php-7.2.19/Zend/zend_vm_execute.h:63410
#52 zend_execute at php-7.2.19/Zend/zend_vm_execute.h:63776
#53 zend_execute_scripts at php-7.2.19/Zend/zend.c:1498
#54 php_execute_script at php-7.2.19/main/main.c:2594
#55 do_cli at php-7.2.19/sapi/cli/php_cli.c:1011
#56 main at php-7.2.19/sapi/cli/php_cli.c:1403

また、gdb の info shared コマンドで確認しても、

From                To                  Syms Read   Shared Object Library
0x00007f7f40200770  0x00007f7f4020bae5  Yes (*)     /usr/libexec/ld.elf_so
0x00007f7d1a885300  0x00007f7d1a992120  Yes         /usr/lib/libcrypto.so.12
...
0x00007f7d0ac2f1c0  0x00007f7d0ac6c630  Yes         /usr/pkg/lib/libmysqlclient.so.20
...

さっきのアドレス 0x7f7d0ac5a12a は、確かに libmysqlclient.so.20 の中(From と To の間)にある。

openssl_lock_function のソースは、mysql の vio/viosslfactories.c にある。)

ここで、続きを実行すると、

(gdb) continue
Continuing.
  - installed: true
  - version: 16.0.1.1
  - versionstring: 16.0.1
  - edition:

Program received signal SIGSEGV, Segmentation fault.
0x00007f7d0ac5a12a in ?? ()

nextcloud の occ status の出力の後、先ほどのアドレスを呼んで(コールバックして)死んだのがわかる。

ここで gdb の bt コマンドで確認すると、php の module_destructor ()から curl を経由して、openssl の ENGINE_cleanup () から始まる一連の終了処理でこうなっていることがわかる。

#0  ?? ()
#1  CRYPTO_lock at openssl/dist/crypto/cryptlib.c:596
#2  engine_table_cleanup at openssl/dist/crypto/engine/eng_table.c:229
#3  engine_unregister_all_RAND at openssl/dist/crypto/engine/tb_rand.c:74
#4  engine_cleanup_cb_free at openssl/dist/crypto/engine/eng_lib.c:198
#5  sk_pop_free at openssl/dist/crypto/stack/stack.c:327
#6  ENGINE_cleanup at openssl/dist/crypto/engine/eng_lib.c:205
#7  Curl_ossl_cleanup at curl-7.65.1/lib/vtls/openssl.c:1098
#8  Curl_ssl_cleanup at curl-7.65.1/lib/vtls/vtls.c:180
#9  curl_global_cleanup at curl-7.65.1/lib/easy.c:268
#10 zm_shutdown_curl at php-7.2.19/ext/curl/interface.c:1419
#11 module_destructor at php-7.2.19/Zend/zend_API.c:2564
#12 module_destructor_zval at php-7.2.19/Zend/zend.c:690
#13 _zend_hash_del_el_ex at php-7.2.19/Zend/zend_hash.c:998
#14 _zend_hash_del_el at php-7.2.19/Zend/zend_hash.c:1021
#15 zend_hash_graceful_reverse_destroy at php-7.2.19/Zend/zend_hash.c:1477
#16 zend_destroy_modules at php-7.2.19/Zend/zend_API.c:2008
#17 zend_shutdown at php-7.2.19/Zend/zend.c:905
#18 php_module_shutdown at php-7.2.19/main/main.c:2453
#19 main at php-7.2.19/sapi/cli/php_cli.c:1418

さらに、もう一度 gdb の info shared コマンドで確認すると、

From                To                  Syms Read   Shared Object Library
0x00007f7f40200770  0x00007f7f4020bae5  Yes (*)     /usr/libexec/ld.elf_so
0x00007f7d1a885300  0x00007f7d1a992120  Yes         /usr/lib/libcrypto.so.12
...
0x00007f7d13206800  0x00007f7d13210f5e  Yes (*)     /usr/lib/libutil.so.7
0x00007f7d09003d50  0x00007f7d09007b92  Yes         /usr/pkg/lib/php/20160303/zlib.so
...

0x00007f7d0ac5a12aの辺りは空で、コールバックしようにも libmysqlclient.so が既にメモリ上にいないことがわかる。

犯人捜し

  • php の extension の初期化/終了処理の順序が悪い
  • php の各 extension の ssl の扱いが統一されていないのが悪い
  • curl が ENGINE_cleanup してしまうのが悪い
  • mysqlclient の openssl まわりの仕様が悪い

などが想定されるが、断定はできないし、どれも微妙にダメな気もする。

mysql 各バージョン毎の違い
mysql 5.6(以前)の vio/viosslfactories.c には、そもそもこの処理がないので問題にならないと考えられる。

また、mysql 5.7 の vio/viosslfactories.c(あるいは mysql 8.0 の vio/viosslfactories.cc)を見ると、

/*
  OpenSSL 1.1 supports native platform threads,
  so we don't need the following callback functions.
*/

とあり、openssl 1.1未満(OPENSSL_VERSION_NUMBER < 0x10100000L)のときだけ、この処理がコンパイルされるので、openssl 1.1系では問題にならないと考えられる。

以上。