SELinux の deny_execmem を有効にすると、動的ライブラリのロードに失敗することがある
はじめに
Linux Advent Calendar 2022 の22日目の記事になります。
SELinuxの動作に関する記事ですが、私はSELinuxに詳しいわけではなく、OSSに報告されたissueの解析結果をまとめた記事になります。
要約
SELinux の deny_execmem を有効にすると
- mprotect(2) にて フラグ PROT_EXEC が立っていると、EACCES(=13) が返り失敗するようになる。
これに伴って下記のような挙動をするようになる。
- GNU_STACKに実行可能属性がついている動的ライブラリのロードに失敗する。
- 上記のライブラリを動的リンクしているプログラムを実行できない。
- ldd(1)やld-linux.so の対象として上記ライブラリやプログラムを指定するとPermission deniedによって失敗する。
- 例:
error while loading shared libraries: libfluentbit.so: cannot enable executable stack as shared object requires: Permission denied
対策としては、動的ライブラリのビルドオプションを変更することで対応を行った。
検証環境
issueで報告された環境である CentOS Stream 9 の x86_64版で動作確認しています。
GUIでdeny_execmemを有効にすると強制ログアウトされ、以降ログイン不能になったため、下記コマンドでCUIログインに変更してから試しています。
# systemctl set-default multi-user.target
deny_execmemの有効化は下記コマンド。
# setsebool -P deny_execmem on
調査に至った背景
Fluent-bitの下記issueからです。
deny_execmemが有効な環境上では、Fluent-bitの動作が途中で止まるというものでした。
なお本件においては、Fluent-bit自体がどのような機能のプログラムなのかは重要でないため、説明は割愛します。
挙動の確認
このissueの動作確認を行う上で幾つかの挙動が見えてきました。なお、事前情報として、Fluent-bitは下記の二種類のバイナリを提供しています。
- fluent-bit
- 実行ファイル。
- libfluentbit.so
- 上記の実行ファイルを動的ライブラリ化したもの。ユーザープログラムからFluent-bitに任意のデータを流しこむケースを想定。
見えてきた挙動は下記のとおりです。
- 実行ファイル(fluent-bit)の動作が途中で止まる
- 動的ライブラリ(libfluentbit.so)をリンクしているプログラムを実行すると、ライブラリのロードがPermission deniedになり、全く動作しない
- strace(1)結果やエラーログから、上記いずれの場合でもmprotect(2)がEACCESエラーで失敗している
なぜ動的ライブラリのロードに失敗したのか
実行時の文言や、gdbでmain関数や__start関数をbreak pointとしても全く引っかからないことから、動的ライブラリとリンクしているプログラムそのものでなく、その手前の処理でエラーになっていることが予想されました。
ということで、glibcの elf/dl-load.c の ソースを確認したところ、下記のような箇所がありました。
if (__glibc_unlikely ((stack_flags &~ GL(dl_stack_flags)) & PF_X))
{
/* The stack is presently not executable, but this module
requires that it be executable. We must change the
protection of the variable which contains the flags used in
the mprotect calls. */
ということで、スタックのフラグにPF_X(=実行可能)な状態がついていると、mprotect(2)
に渡す変数を変更して実行可能とするようです。つまりこの処理によって、動的リンカ内でmprotect(2)
にフラグ PROT_EXEC
が渡されることになり、失敗するということになります。
実行可能なスタックを要求するライブラリの調べ方
readelf(1)
で確認できるようです。下記のようにGNU_STACKにEが立っているかどうかで挙動が変わるようでした。
$ readelf -Wa libfluent-bit.so |grep GNU_STACK
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RWE 0x10
GNU_STACKに実行属性が付いた契機
最近のアップデートにて、Fluent-bitはWASMをサポートするべく、WAMR(WebAssembly Micro Runtime)を静的リンクするようになりました。 どうもこれによって、GNU_STACKに実行属性が付与されるようになったようでした。
WAMRについては、ビルド時の設定によってリンクしない構成も可能でした。よって、切り分けの一環としてWAMRのリンクを行わない設定としたところ、deny_execmem環境下でも、意図通りに動作することがわかりました。
対策
対策としては、動的ライブラリのGNU_STACKにおける実行属性を落とすことになるかと思います。(もしくは暫定策として、当該ライブラリを動的リンクしないようにし、使わないようにする。)
なお、Fluent-bitにおいては、WAMRのリポジトリにおいて下記記載があり、またこの一部にnoexecstackが含まれていることから、この設定を有効にすることで解消されないか検討を行いました。
現在では、下記のパッチがマージされており、新規に追加されたビルドオプションによって、deny_execmemの設定が有効であってもWAMRが組み込まれたFluent-bitが動作するようになりました。
おまけ Linux Kernelの検知箇所
(きちんと調べていません。メモ程度)
検証環境と同バージョンのLinux Kernel v5.14 のソースを確認したところ、security/selinux/hooks.c 内の file_map_prot_check
という関数にて下記の実装がありました。
この関数は同ソース内のselinux_file_mprotect
という関数から呼ばれており、関連がありそうです。なお、selinux_mmap_file
という関数からも呼ばれており、mmapにも影響があることが予想されます。
まとめ
繰り返しになりますが、deny_execmemが有効な環境においては、
- フラグ PROT_EXECが立ったmprotect(2)がEACCESで失敗する
- GNU_STACKに実行可能属性がついた動的ライブラリをリンクしたプログラムは全く動作しない
- 上記のプログラムに対してldd(1)を使うとエラーが出て失敗する
- GNU_STACKに実行可能属性がついた実行ファイルについては起動はする
ようです。
このうち2.と3.の振る舞いについてはglibcの動的リンカの仕様によって、1.を行うためでした。
また、GNU_STACKの属性については、readelf -Wa *.so|grep GNU_STACK
や readelf -l *.so
によって調べられます。
対策としては、GNU_STACKに実行可能属性が付かないように動的ライブラリのビルドを行う、もしくはそのようなライブラリをリンクしない、になるかと思います。
参考情報
Discussion