条件が揃うと、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.c
の ssl_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系では問題にならないと考えられる。
以上。