Linux システムコールは、プロセスとカーネルの間にある、アーキテクチャ固有のバイナリインターフェースを形成します。アプリケーションコードは通常、代わりに C ライブラリ API を対象にします。これは、ライブラリが型、feature-test マクロ、互換性動作、およびマシン固有のエントリシーケンスを提供するためです。
ライブラリ API とカーネル ABI #
open(path, flags) のようなソースレベルの呼び出しは、open という名前のカーネルシステムコールを実行することを保証するものではありません。glibc はこれを openat で実装したり、キャンセルチェックを追加したり、時間幅互換のエントリポイントを選択したりできます。clock_gettime のような関数は、カーネルに入らずに vDSO 内で実行できます。逆に Linux は、古い C ライブラリがラッパーを提供していなかった gettid のようなシステムコールを公開しています。
カーネル ABI は次の要素で構成されます。
- プロセスのアーキテクチャおよび ABI に対応するシステムコール番号;
- レジスタで表現される引数。一部の ABI では、引数の一部がスタック上に置かれます;
syscall、int $0x80、svc、またはecallのようなエントリ命令; および- 成功およびエラーを表す生の戻り規約。
<sys/syscall.h> の SYS_name 定数は、対象ビルド ABI における番号を識別します。これらはアーキテクチャ間で安定していません。x86-64 の write は 1 ですが、i386 の write は 4 です。Compat および x32 のプロセスは、同じカーネル上でも別のテーブルを使用できます。カーネルは、その ABI の syscall table を通じて番号をディスパッチし、通常は SYSCALL_DEFINE* マクロで宣言された実装を呼び出します。
レジスタ、結果、および errno
#
一般的な Linux x86 エントリ ABI の 2 つは、対応関係を次のように示します。
| ABI | エントリ | 番号 | 引数 1~6 | 結果 |
|---|---|---|---|---|
| x86-64 | syscall | rax | rdi, rsi, rdx, r10, r8, r9 | rax |
| i386 | int $0x80 | eax | ebx, ecx, edx, esi, edi, ebp | eax |
x86-64 の命令は rcx と r11 を上書きします。そのため、4 番目の引数には、C 関数 ABI の rcx ではなく r10 を使用します。ほかのアーキテクチャには、異なるアラインメント規則やレジスタペア規則があります。たとえば、一部の 32 ビット ABI では、64 ビット引数を分割してアラインメントします。
x86 では、カーネルは結果レジスタの -1 から -4095 までの範囲にある負の値としてエラーを報告します。libc ラッパーは、その生の結果を -1 に変換し、正のエラー番号をスレッドローカルな errno に格納します。エラーインジケーターを別途持つアーキテクチャでは、libc ラッパーがその差異を正規化します。したがって、コードは呼び出し成功後に errno を読み取るのではなく、文書化されたラッパーの戻り値を検査する必要があります。
syscall の直接使用
#
glibc は、通常のラッパーが存在しないエントリに対して間接ラッパーを提供します。
#define _GNU_SOURCE
#include <sys/syscall.h>
#include <unistd.h>
long tid = syscall(SYS_gettid);
if (tid == -1) {
/* errno describes the failure. */
}プロトタイプは long syscall(long number, ...); です。この関数はアーキテクチャのエントリシーケンスを実行し、カーネルエラーを errno に変換しますが、可変長引数であるため型チェックは行われません。値を正確な ABI 幅にキャストし、64 ビット引数を分割する場合はアーキテクチャ固有の規則に従ってください。実行中のカーネルがその番号を実装していない場合、新しい呼び出しは ENOSYS を返すこともあります。
通常の libc ラッパーが存在する場合は、それを優先してください。直接使用すると、次の機能を迂回する可能性があるためです。
- libc が実装する構造体および時間幅の互換性;
- ラッパーが定義する、古いシステムコールへのフォールバック;
- POSIX スレッドキャンセルの動作; および
- vDSO による高速化。
インラインアセンブリを生で使用すると、レジスタ制約、破壊されるレジスタやメモリ、メモリ順序付け、およびエラー変換に関する危険が加わります。C ライブラリまたは別の ABI レベルのランタイムを実装する場合に限り、使用が正当化されます。
アーキテクチャとバージョンの移植性 #
カーネルシステムコールは、より高レベルの標準がライブラリの動作も定義していない限り、Linux 固有です。コンパイル時の SYS_name の利用可能性が証明するのは、ヘッダーが ABI を記述していることだけであり、実行中のカーネルがその呼び出しを実装していることではありません。呼び出しを実行して ENOSYS を処理することで確認し、存在する場合は意味的に正しいフォールバックを使用してください。
別のアーキテクチャから番号をコピーしたり、クロスコンパイルされたプログラムにホスト用のテーブルをハードコードしたりしないでください。境界を通過する構造体も、選択した ABI のフィールド幅、アラインメント、エンディアン、および UAPI 定義に依存します。Seccomp と audit が同じ理由でアーキテクチャトークンと番号の両方を識別するのは、このためです。
再起動、キャンセル、トレース、および Seccomp #
シグナルはブロッキング呼び出しを中断できます。ユーザー空間へ戻る間、カーネルは内部の -ERESTART* 結果を使用して呼び出しを再発行するかどうかを判断します。ユーザー空間からは通常、呼び出しが完了した状態、または -1/EINTR のいずれかが見えます。SA_RESTART、対象のシステムコール、およびデータがすでに転送されたかどうかによって結果が決まります。アプリケーションは、操作の API が再試行を安全にしている場合に限り EINTR を再試行し、再試行前に正の短い結果を処理する必要があります。
POSIX スレッドキャンセルは、カーネルシステムコールの特性ではなく libc の特性です。キャンセルポイント用の glibc ラッパーは、ブロッキング操作の前後でキャンセルを調整します。syscall() を直接呼び出すと、その調整を迂回する可能性があります。
strace と ptrace は、カーネルへのエントリと終了、引数、および生の結果を監視します。vDSO だけで完全に処理される呼び出しは、syscall stop を生成しません。トレーサーは番号、引数、または結果を変更することもできるため、トレースされた動作は独立したセキュリティ境界ではありません。
Seccomp BPF は、カーネルで実行される前に、システムコールのアーキテクチャ、番号、および生の引数値を評価します。番号空間は重複するため、堅牢な allowlist は番号だけでなくアーキテクチャトークンも検査します。フィルターはポインター引数をデリファレンスできません。また、libc のアップグレードによって、API 名から示唆されるものとは異なる基盤システムコールが選択される可能性があります。したがって Seccomp はカーネルの攻撃対象領域を最小化しますが、完全なサンドボックスを構成するものではありません。
アップストリームの syscall(2) ページにはエントリおよびレジスタの規約が記載されており、カーネルの seccomp documentation ではフィルタリングのセマンティクスが定義されています。
コメント
GitHub で閲覧・コメントする(投稿にはログインが必要です)