【Vol2. 原因分析】 CentOS7 で glibc をアップデート後、 mod_php + httpd の logrotate (reload ) が失敗する: /lib64/libresolv.so.2: symbol __h_errno, version GLIBC_PRIVATE not defined in file libc.so.6 with link time reference

hiboma.hatenadiary.jp

の続き。

本エントリのサマリ

  • CentOS7 で httpd + mod_php が起動している
  • glibc のアップデートをすると __h_errno シンボルが増える
  • httpd の reload をしても httpd プロセスが古い glibc を参照しつづており、libphp5 -> libresolv.so が依存する __h_errno シンボルを解決できない
  • 結果、httpd が reload 失敗 する

という現象をおった. 以下はその詳細

前回のログを調べ直す

前回のエントリでは下記のログを調べていた

/lib64/libresolv.so.2: symbol __h_errno, version GLIBC_PRIVATE not defined in file libc.so.6 with link time reference

symbol __h_errno が未定義だと言っているので、素直にそのシンボルを調べ直した

__h_errnor のシンボルを調査する

glibc のアップデート前後でシンボルを採取して比較してみたところ、アップデート後に __h_errno のシンボルが増えていた

glibc アップデート前

再現を取った時のバージョンは glibc-2.17-196.el7.x86_64

$ nm          /lib64/libresolv.so.2 | grep __h_errno
$ objdump -Tt /lib64/libresolv.so.2 | grep __h_errno
$ nm /lib64/libc.so.6 | grep __h_errno
0000000000110390 t __GI___h_errno_location
0000000000110390 T __h_errno_location
$ objdump -Tt /lib64/libc.so.6  | grep __h_errno
0000000000110390 l     F .text 0000000000000011              __GI___h_errno_location
0000000000110390 g     F .text 0000000000000011              __h_errno_location
0000000000110390 g    DF .text 0000000000000011  GLIBC_2.2.5 __h_errno_location

glibc アップデート後

再現を取った時のバージョンは glibc-2.17-196.el7.x86_64.

$ nm /lib64/libresolv.so.2 | grep __h_errno
                U __h_errno@@GLIBC_PRIVATE
$ objdump -Tt /lib64/libresolv.so.2  | grep __h_errno
0000000000000000         *UND* 0000000000000000              __h_errno@@GLIBC_PRIVATE 👈
0000000000000000      D  *UND* 0000000000000000  GLIBC_PRIVATE __h_errno 👈
$ nm /lib64/libc.so.6 | grep __h_errno
0000000000117ff0 t __GI___h_errno_location
000000000000008c B __h_errno 👈
0000000000117ff0 T __h_errno_location
$ objdump -Tt /lib64/libc.so.6  | grep __h_errno
0000000000117ff0 l     F .text  0000000000000011              __GI___h_errno_location
0000000000117ff0 g     F .text  0000000000000011              __h_errno_location
000000000000008c g       .tbss  0000000000000004              __h_errno 👈
000000000000008c g    D  .tbss  0000000000000004  GLIBC_PRIVATE __h_errno 👈
0000000000117ff0 g    DF .text  0000000000000011  GLIBC_2.2.5 __h_errno_location

👈 に指した __h_errno が増えている

httpd を起動してる状態で glibc をアップデートすると何が起きるか

httpd を起動してる状態で glibc をアップデートすると何が起きているか? lsof で見るとわかるのだが、httpd は古い libc.so と libresolv.so を参照し続けている

$ sudo lsof -p $(pgrep httpd | head -1) | grep -e libresolv -e libc-2.17
httpd   2843 root  DEL    REG              253,0           33566307 /usr/lib64/libresolv-2.17.so;5c530fe7
httpd   2843 root  DEL    REG              253,0           33562151 /usr/lib64/libc-2.17.so;5c530fe7
  • DEL = ファイルシステムからパスは消えているが、参照カウントが残っているのでファイルの実体は残っている
  • 古い .so を参照し続けるのは、(確か) RPM が rename(2) で新しい glibc に置き換えるから.

pmap(3) でもよい

mmap(2) している仮想アドレスも把握したいのであれば pmap(3) を使う

$ sudo pmap -p $(pgrep httpd | head -1) | grep -e libresolv -e libc-2.17
00007f193f553000     88K r-x-- /usr/lib64/libresolv-2.17.so;5c530fe7 (deleted)
00007f193f569000   2048K ----- /usr/lib64/libresolv-2.17.so;5c530fe7 (deleted)
00007f193f769000      4K r---- /usr/lib64/libresolv-2.17.so;5c530fe7 (deleted)
00007f193f76a000      4K rw--- /usr/lib64/libresolv-2.17.so;5c530fe7 (deleted)
00007f194b9d3000   1760K r-x-- /usr/lib64/libc-2.17.so;5c530fe7 (deleted)
00007f194bb8b000   2048K ----- /usr/lib64/libc-2.17.so;5c530fe7 (deleted)
00007f194bd8b000     16K r---- /usr/lib64/libc-2.17.so;5c530fe7 (deleted)
00007f194bd8f000      8K rw--- /usr/lib64/libc-2.17.so;5c530fe7 (deleted)

観測: httpd を realod すると?

httpd を reload する際の strace をとると、古い /lib64/libresolv.so.2 や libphp5.so をアンロード ( munmap(2) )し、再度ロードするのが観測できる

アンロードの様子

reload している際に munmap(2) で観測できる

/* strace で munmap だけに絞り込んでいる */

munmap(0x7f67113ac000, 1052672)         = 0
munmap(0x7f67114ad000, 266240)          = 0
munmap(0x7f671135d000, 323584)          = 0
munmap(0x7f6703797000, 6742568)         = 0
munmap(0x7f6703305000, 2584736)         = 0
munmap(0x7f67030ec000, 2198200)         = 0
munmap(0x7f6702d82000, 3575896)         = 0

要注意なのが、 httpd の reload 時に glibc はアンロードされないのだ

再ロードの様子

/* strace で open, mmap だけに絞り込んでいる */

16:50:58 open("/etc/httpd/modules/libphp5.so", O_RDONLY|O_CLOEXEC) = 8</usr/lib64/httpd/modules/libphp5.so> <0.000008>
16:50:58 mmap(NULL, 6742568, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 8</usr/lib64/httpd/modules/libphp5.so>, 0) = 0x7fcc710b6000 <0.000007>
16:50:58 mmap(0x7fcc71676000, 606208, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 8</usr/lib64/httpd/modules/libphp5.so>, 0x3c0000) = 0x7fcc71676000 <0.000008>
16:50:58 mmap(0x7fcc7170a000, 107048, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fcc7170a000 <0.000009>
16:50:58 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 8</etc/ld.so.cache> <0.000009>
16:50:58 mmap(NULL, 19484, PROT_READ, MAP_PRIVATE, 8</etc/ld.so.cache>, 0) = 0x7fcc7ee1b000 <0.000006>
16:50:58 open("/lib64/libresolv.so.2", O_RDONLY|O_CLOEXEC) = 8</usr/lib64/libresolv-2.17.so> <0.000008>

...

httpdetc/httpd/modules/libphp5.so を dleopen(3) して、ld.so が依存の共有ライブラリを解決していく

時系列の整頓

調べたことを時系列に起こす

1. httpd + mod_php が起動している

f:id:hiboma:20190201123743p:plain

2. glibc をアップデートする

httpd古い glibc を参照し続ける

f:id:hiboma:20190201123805p:plain

2. httpd を reload する

httpd の親プロセスは pid を変えずに reload を始める

  • httpd の親プロセスは 古い glibc を参照したまま libphp5.so をロードし直す
  • libphp5.so をロードする際に、依存している /lib64/libresolv.so.2 をロードする
  • /lib64/libresolv.so をロードする際に symbol __h_errno を解決しようとするが、 httpd は古い glibc を参照したまま であり、古い glibc では __h_errno を解決できない

エラーとして扱われ、上位の httpd 層まで伝播して Syntax Error となる

f:id:hiboma:20190201123823p:plain

ということだろうか? とアタリをつけた

LD_DEBUG=symbols,bindings で追いかける

LD_DEBUG オプションの存在を思い出したので、これでデバッグログをとり確認を取った

/* sudo vim /etc/sysconfig/httpd */
LD_DEBUG=symbols,bindings

LD_DEBUG 環境変数をセットして再現をとりなおすと /var/log/httpd/error_log に大量のログがでてくる

      2841:  symbol=__h_errno;  lookup in file=/usr/sbin/httpd [0]
      2841: symbol=__h_errno;  lookup in file=/lib64/libpcre.so.1 [0]
      2841: symbol=__h_errno;  lookup in file=/lib64/libselinux.so.1 [0]
      2841: symbol=__h_errno;  lookup in file=/lib64/libaprutil-1.so.0 [0]
      2841: symbol=__h_errno;  lookup in file=/lib64/libcrypt.so.1 [0]
      2841: symbol=__h_errno;  lookup in file=/lib64/libexpat.so.1 [0]
      2841: symbol=__h_errno;  lookup in file=/lib64/libdb-5.3.so [0]
      2841: symbol=__h_errno;  lookup in file=/lib64/libapr-1.so.0 [0]
      2841: symbol=__h_errno;  lookup in file=/lib64/libpthread.so.0 [0]
      2841: symbol=__h_errno;  lookup in file=/lib64/libdl.so.2 [0]
👉    2841:   symbol=__h_errno;  lookup in file=/lib64/libc.so.6 [0]
🔥    2841:   /lib64/libresolv.so.2: error: relocation error: symbol __h_errno, version GLIBC_PRIVATE not defined in file libc.so.6 with link time reference (fatal)

ダイナミックリンカ? が __h_errno のシンボルを順に探していく様子、 /lib64/libc.so.6 で lookup が失敗しているのがトレースできた

まとめ

  • 当初は別のバグを疑ってかかっていて、シンボルを調べることをしなかった ( 同僚である zipper が最初にそれを指摘してくれていたのだがスルーしてしまった)
  • 特殊な条件が重なって怒るので「バグ」とも言えない、びみょーな不具合だなぁ

CentOS7 で glibc をアップデート後、 mod_php + httpd の logrotate (reload ) が失敗する: /lib64/libresolv.so.2: symbol __h_errno, version GLIBC_PRIVATE not defined in file libc.so.6 with link time reference

CentOS7 で glibc をアップデート後、mod_php + httpd の logrotate(reload) に失敗する という内容を追いかけるエントリです

(追記)

社内で共有してみたところ、前に踏んだ && 対応してたという事例もあがってきた。よくある問題なのかなぁ

経緯

社内の同僚から「Apache が logrotate 時にコケて困った」という相談を受けてログを見ていた

エラーログ

CentOS7 のホストで、こんなログがでていたと教えてもらった (これはリアルなログ)

[Sun Jan 27 03:36:01.359870 2019] [mpm_prefork:notice] [pid 32379] AH00171: Graceful restart requested, doing restart httpd: Syntax error on line 39 of /etc/httpd/conf/httpd.conf: Syntax error on line 1 of /etc/httpd/conf.modules.d/php7.load: Cannot load modules/libphp7.so into server: /lib64/libresolv.so.2: symbol __h_errno, version GLIBC_PRIVATE not defined in file libc.so.6 with link time reference

付随する情報として

  • このログを出して logrotate 時の reload が失敗して httpd プロセスが死んでしまう
  • 改めて systemctrl start httpd すると何事もなく起動する
  • 複数のホストで発生した ( production, staging 環境 etc )

も教えてもらった

何が問題か? 推論を立てていく

エラーログや glibcyum アップデートの時刻をまとめてみたところ

  • httpd + mod_php が起動している状態だった
  • glibc をアップデートしていた
  • 早朝の logrotate 時に冒頭のエラーを引き起こしていた

を突き止めた

以上を元にして Vagrant で再現を試みた

再現コード

このブログを書いている時点では、以下の Vagrantfile で再現が取れる

# -*- mode: ruby -*-

Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"
  config.vm.provision "shell", inline: <<-SHELL
yum install -y httpd php lsof gdb
systemctl start httpd
yum install -y glibc
rpm -q glibc
rpm -q httpd
systemctl reload httpd.service
cat /var/log/httpd/error_log
SHELL
end

上記を Vagrantfile として書いて vagrant up 実行で冒頭のエラーログを再現できる

再現時のログ

[vagrant@localhost ~]$ sudo cat /var/log/httpd/error_log
[Tue Jan 29 15:52:05.240309 2019] [core:notice] [pid 2832] SELinux policy enabled; httpd running as context system_u:system_r:httpd_t:s0
[Tue Jan 29 15:52:05.240883 2019] [suexec:notice] [pid 2832] AH01232: suEXEC mechanism enabled (wrapper: /usr/sbin/suexec)
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using localhost.localdomain. Set the 'ServerName' directive globally to suppress this message
[Tue Jan 29 15:52:05.254153 2019] [auth_digest:notice] [pid 2832] AH01757: generating secret for digest authentication ...
[Tue Jan 29 15:52:05.254796 2019] [lbmethod_heartbeat:notice] [pid 2832] AH02282: No slotmem from mod_heartmonitor
[Tue Jan 29 15:52:05.264794 2019] [mpm_prefork:notice] [pid 2832] AH00163: Apache/2.4.6 (CentOS) PHP/5.4.16 configured -- resuming normal operations
[Tue Jan 29 15:52:05.264816 2019] [core:notice] [pid 2832] AH00094: Command line: '/usr/sbin/httpd -D FOREGROUND'
🔥 [Tue Jan 29 15:53:02.467917 2019] [mpm_prefork:notice] [pid 2832] AH00171: Graceful restart requested, doing restart
httpd: Syntax error on line 56 of /etc/httpd/conf/httpd.conf: Syntax error on line 6 of /etc/httpd/conf.modules.d/10-php.conf: Cannot load modules/libphp5.so into server: /lib64/libresolv.so.2: symbol __h_errno, version GLIBC_PRIVATE not defined in file libc.so.6 with link time reference

再現した環境で、問題に関係しそうなパッケージのバージョンは以下の通りになる

バグレポートを探す

bugzilla.redhat.com

上記のバグレポートが本エントリに書かれたのとほぼ一緒の内容だ。が、SL7 パッケージの不具合報告を RedHat に投げているので取り扱ってもらえていない ...

glibc を深追いする (未解決)

glibc-2.17-260.el7.src.rpm でエラーメッセージの出所を探る. with link time reference をキーワードにした. glibc の _dl_lookup_symbol_x() がエラーの出どころだった

/* Search loaded objects' symbol tables for a definition of the symbol
   UNDEF_NAME, perhaps with a requested version for the symbol.

   We must never have calls to the audit functions inside this function
   or in any function which gets called.  If this would happen the audit
   code might create a thread which can throw off all the scope locking.  */
lookup_t
internal_function
_dl_lookup_symbol_x (const char *undef_name, struct link_map *undef_map,
                     const ElfW(Sym) **ref,
                     struct r_scope_elem *symbol_scope[],
                     const struct r_found_version *version,
                     int type_class, int flags, struct link_map *skip_map)
{

...


  /* Search the relevant loaded objects for a definition.  */
  for (size_t start = i; *scope != NULL; start = 0, ++scope)
    {
      int res = do_lookup_x (undef_name, new_hash, &old_hash, *ref,
                             &current_value, *scope, start, version, flags,
                             skip_map, type_class, undef_map);
      if (res > 0)
        break;

      if (__builtin_expect (res, 0) < 0 && skip_map == NULL)
        {
          /* Oh, oh.  The file named in the relocation entry does not
             contain the needed symbol.  This code is never reached
             for unversioned lookups.  */
          assert (version != NULL);
          const char *reference_name = undef_map ? undef_map->l_name : NULL;

          /* XXX We cannot translate the message.  */
          _dl_signal_cerror (0, (reference_name[0]
                                 ? reference_name
                                 : (rtld_progname ?: "<main program>")),
                             N_("relocation error"),
                             make_string ("symbol ", undef_name, ", version ",
                                          version->name,
                                          " not defined in file ",
                                          version->filename,
                                          " with link time reference", 👈
                                          res == -2
                                          ? " (no version symbols)" : ""));
          *ref = NULL;
          return 0;
        }
    }

dlopen(3) の内部実装の箇所で、おそらく、動的リンク( relocation? ) する際にシンボル定義の探索をしているような処理に思うのだけど.

glibc は強すぎて全然分からない ...

まとめ

一過性の障害なので エラーの詳細をじっくりと時間をかけて解析したいモチベーションが無いかもしれない.

まずは別の環境でどうやって回避するか、かなぁ ...

お疲れ様です

zipper とボブさん、レポートありがとうございました && 対応お疲れ様です sugy さん、過去の事例共有ありがとうございます

『組織事故』『組織事故とレジリエンス』ジェームズ・リーズン(著) 📚

年末年始に以下の著作を購入し読んでいた。書評を書くほどまでには咀嚼できていないので、書籍の内容の紹介は一切ない。御了承を :)

組織事故―起こるべくして起こる事故からの脱出

組織事故―起こるべくして起こる事故からの脱出

組織事故とレジリエンス―人間は事故を起こすのか、危機を救うのか

組織事故とレジリエンス―人間は事故を起こすのか、危機を救うのか

どちらも同じ著者で重複する内容もあるのだが、翻訳・構成・挿絵がカッチリと書かれているのは古い方の『組織事故』の方だなという印象

日々イベント 🔥

現在 GMOペパボのセキュリティ対策室なる組織で業務をしているのだが、日々 大小のイベント ( セキュリティ ≒ 機密性・完全性 を脅かすもの以外にも、いわゆる障害 ≒ 可用性を脅かすものも何でも ) に遭遇している。

対策していても何か起きてしまったら どうするか?

組織を脅かすイベントが起きる前に十全に対策を重ねておくのは当然だ!!!1 そりゃそうだねと断ったうえで ...

そうではあっても避けようもない何らかのイベントは起きてしまうもので、では どうすれば 組織が迅速にイベントの発生を検知し、組織がイベントにより平衡を失った状態からリカバリーし、組織がイベントの損失を学習・アクションに転換していけるのか? というテーマでコミットできないかなぁと考えている

... とまぁ、そんなこともあっていろいろ読んでみるかと、冒頭の書籍を手にした次第

次に読みたい

下記の書籍も私の興味に答えてくれそうな中身っぽいので 次、手に取ろうと画策している

レジリエンスエンジニアリング―概念と指針

レジリエンスエンジニアリング―概念と指針

レジリエンス」という単語がややバズワード感あるので心配になるが、そういう本では無いようだ


ところで Google SRE 本の Postmortem Culture: Learning from Failure は 失敗(障害) から学習を達成するためのプラクティスとして よく知られたものだろう。

弊社でも 昨年初めより Postmortem を取り入れていて、ゆっくりと時間をかけて技術者の中で文化・プロセスとして根付いて来ているのを実感している ( 偉そうに書いたが私が提案・推進した訳じゃないので かしこ )

landing.google.com

Postmortem もそうなんだけど、なかなかこういった組織的な文化・プロセス作りは一人で発起しても進まないもの。会社の中で取り組んでいくなら同士を募って進めたいところ。

『組織事故』『レジリエンスエンジニアリング』誰か一緒に読まない?

時坂峠 〜 都民の森 〜 風張峠 〜 奥多摩 🚴‍♂️

10/28(日) の記録。しばらく体調や天候にめぐまれずロングで走れて無かったけど、 久々に距離と獲得標高をしっかりと稼いで走ってきた

走行距離 165km, 獲得標高 1714m

続きを読む

Linux Kernel CVE-2018-5390 SegmentSmack の PoC を書いて分析・検証した

表題の通り Linux Kernel に付いた CVE-2018-5390 / SegmentSmack の PoC を書いて検証・観察をしていた

問題の再現は何とかできたが、 exploit = 悪用可能なレベルまでのコードを書くのが難しい ( 悪用厳禁 )

⚠️

2018年8月に SegmentSmack として騒ぎになった CVE です

続きを読む

【続 Vol.3 】ClamAV - clamd の OnAccessExtraScanning 機能のメモリリークをレポートした - 0.1002 が出た

hiboma.hatenadiary.jp

hiboma.hatenadiary.jp

↑ のようなエントリを書いていたが、0.100.2 がリリースされて次の展開を見せた

blog.clamav.net

On-Access "Extra Scanning," an opt-in minor feature of OnAccess scanning on Linux systems, has been disabled due to a known issue with resource cleanup OnAccessExtraScanning will be re-enabled in a future release when the issue is resolved. In the mean-time, users who enabled the feature in clamd.conf will see a warning informing them that the feature is not active. For details, click here.

OnAccessExtraScanning は一旦 0.100.2 で disabled ( warning が出て機能は動かない) になり、また未来の featrure リリースで復活するそうです

感想

名前が載って嬉しいね 🤗

f:id:hiboma:20181004073714p:plain

Linux Kernel CVE-2017-18017 の Poc を書いて検証した

表題の通り CVE-2017-18017 の PoC を書いてどのような影響があるのを検証・観察した

⚠️

一年以上前に修正パッチが出ている CVE です

CVE の Description

The tcpmss_mangle_packet function in net/netfilter/xt_TCPMSS.c in the Linux kernel before 4.11, and 4.9.x before 4.9.36, allows remote attackers to cause a denial of service (use-after-free and memory corruption) or possibly have unspecified other impact by leveraging the presence of xt_TCPMSS in an iptables action.

iptablesTCPMSS ターゲット ( --set-mss または --clamp-mss-to-pmtu ) を使っているホストに、細工した TCPヘッダを投げつけると use-after-free やメモリの破壊等々が起こせる

CVSS3 の Base Score が 9.8 と高い 🔥

続きを読む