このページの目次

シグナルと ptrace

カバー範囲: シグナルのライフサイクル (生成→配信→処理) → signal_struct/sighand_struct → 信頼性のある/ないシグナル → ptrace メカニズム (PTRACE_ATTACH/PTRACE_SYSCALL/PTRACE_PEEKDATA) → seccomp 排他制御 カーネルバージョン: 2.6 ~ 6.x

概要

シグナルは Unix において最も古い IPC(プロセス間通信)メカニズムの一つであり、統一された例外/イベント通知フレームワークとしても機能します。ユーザーが Ctrl-C を押して SIGINT を発生させる場合、ゼロ除算によって SIGFPE がトリガーされる場合、子プロセスの終了に伴って SIGCHLD が発生する場合など、すべてが同じカーネルパスを通過します。

ptrace は、デバッガ(gdb、strace)やサンドボックス(seccomp user notification)の基盤となるシステムコールであり、あるプロセスが別のプロセスの実行を観察・制御することを可能にします。


シグナルのデータ構造

シグナルセット (sigset_t)

// include/linux/signal_types.h
// シグナルセットはビットマップであり、各ビットがシグナル (1~64) を表す
typedef struct {
    unsigned long sig[_NSIG_WORDS];  // _NSIG=64, _NSIG_WORDS=2 (64ビット環境)
} sigset_t;

// 一般的な操作
sigemptyset(&set);           // 0でクリア
sigaddset(&set, SIGINT);     // 追加
sigdelset(&set, SIGTERM);    // 削除
sigismember(&set, SIGKILL);  // 検査

プロセスごとのシグナル構造体

// include/linux/sched.h
struct task_struct {
    struct signal_struct    *signal;   // スレッドグループで共有
    struct sighand_struct   *sighand;  // スレッドグループで共有 (CLONE_SIGHANDを使用しない限り)
    sigset_t                blocked;   // スレッドごとに独立したブロック済みシグナルセット
    sigset_t                real_blocked;
    struct sigpending       pending;   // スレッドごとの保留中シグナル
};

// include/linux/sched/signal.h
struct signal_struct {
    // スレッドグループで共有される状態
    int                     nr_threads;
    struct sigpending       shared_pending;  // スレッドグループの全スレッドに送信されるシグナル
    // fork/exec の統計情報
    // リソース制限
};

struct sigpending {
    struct list_head        list;       // 保留中シグナルの sigqueue リスト
    sigset_t                signal;     // ビットマップによる高速検索
};

// include/linux/sched/signal.h
struct sighand_struct {
    refcount_t              count;      // リファレンスカウント
    spinlock_t              siglock;    // シグナル処理テーブルを保護
    wait_queue_head_t       signalfd_wqh;
    struct k_sigaction      action[_NSIG];  // 各シグナルの処理アクション
};

sigaction — シグナル処理アクション

// include/linux/signal_types.h
struct k_sigaction {
    struct sigaction sa;  // sa_handler / sa_sigaction / sa_mask / sa_flags を含む
};

// sa_flags の重要なビット:
//   SA_NOCLDSTOP: 子プロセスが停止した際に SIGCHLD を受信しない
//   SA_NOCLDWAIT: 子プロセスの終了時に自動的に回収され、ゾンビが発生しない
//   SA_SIGINFO:   3引数ハンドラ (sig, siginfo, context) を使用
//   SA_RESTART:   シグナルによって中断されたシステムコールを自動的に再試行
//   SA_NODEFER:   シグナル処理中にそのシグナルを自動的にブロックしない
//   SA_RESETHAND: 1回処理後に SIG_DFL に戻す (1回限りの実行)

シグナルのライフサイクル

生成 (generate) → 保留 (pending) → 配信 (deliver) → 処理 (handle)

1. 生成: send_signal()

// kernel/signal.c
int send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
                enum pid_type type) {
    struct sigpending *pending;

    if (type == PIDTYPE_PID)
        pending = &t->pending;              // 特定のスレッドへ送信
    else
        pending = &t->signal->shared_pending;  // スレッドグループ全体へ送信

    // 同じシグナルが既に保留中かチェック (信頼性のないシグナルの重複排除)
    if (legacy_queue(pending, sig))
        goto ret;  // 信頼性のないシグナル: ビットマップのみ記録し、キューには追加しない

    // 信頼性のあるシグナル (SIGRTMIN ~ SIGRTMAX): sigqueue を割り当ててキューに追加
    //   → 同じシグナルでも、送信ごとにキューに追加される
    //   → 消失しない
    __sigqueue_alloc(sig, t, GFP_ATOMIC, ...);

    // pending ビットマップを設定 → ユーザーモード復帰時にチェック
    sigaddset(&pending->signal, sig);

    // TIF_SIGPENDING を設定 → ユーザーモード復帰前にシグナル処理をトリガー
    set_tsk_thread_flag(t, TIF_SIGPENDING);
}

2. 信頼性のないシグナル vs 信頼性のあるシグナル

従来のシグナル (1~31, "信頼性のない"):
  - 同じシグナルを複数回送信 → pending ビットマップには 1 回しか記録されない
  - カーネルがビットマップが既に設定されていることを検出 → 新しい送信は破棄される
  - 古典的な問題: SIGCHLD で複数の子プロセスが同時に終了 → 通知は 1 回だけ受信

リアルタイムシグナル (SIGRTMIN=34 ~ SIGRTMAX=64, "信頼性のある"):
  - 送信ごとに sigqueue を割り当ててキューに追加
  - 追加データ (siginfo.si_value) を保持可能
  - シグナル番号の昇順で配信 (低い番号が優先)
  - 同じ番号のシグナルは FIFO (先入れ先出し)

3. 配信: get_signal() → handle_signal()

// arch/x86/kernel/signal.c
// ユーザーモード復帰前 (exit_to_user_mode_loop) に呼び出される:
bool get_signal(struct ksignal *ksig) {
    // 1. pending からブロックされていないシグナルを 1 つ取得
    //    まずスレッドの pending をチェック → 次に shared_pending をチェック
    //    シグナルは番号の小さい順に取得
    //
    // 2. ptrace によるインターセプト処理 (該当する場合)
    //    ptrace-stop 状態にある場合 → ptracer によって消費されたか確認
    //
    // 3. cgroup シグナルの処理 (該当する場合)
    //
    // 4. SIGKILL / SIGSTOP の処理 (ブロックやハンドラによる上書きは不可)
    //
    // 5. sa_handler のチェック:
    //    - SIG_IGN → 直接破棄
    //    - SIG_DFL → デフォルトアクション (core/stop/ignore/terminate)
    //    - カスタムハンドラ → ユーザーモードスタックフレームを設定し、ハンドラアドレスを返す
    //
    // 6. ksig->ka (sigaction) と ksig->info (siginfo) を設定
    return true;
}

// ハンドラが SIG_DFL/SIG_IGN 以外の場合:
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs) {
    // 1. ユーザーモードスタック上に sigframe を作成
    //    → 現在のレジスタを保存: RIP, RSP, RFLAGS, 汎用レジスタ
    //    → siginfo を保存 (si_signo, si_code, si_addr など)
    //    → FPU 状態を保存 (XSAVE)
    //
    // 2. regs を修正:
    //    regs->rip = sa_handler    → ユーザーモード復帰時にシグナル処理関数へジャンプ
    //    regs->rsp = sigframe      → スタックはシグナルフレームの上にある
    //
    // 3. ユーザーモードへ復帰 → プロセスはシグナルハンドラの実行を開始
}

4. シグナルからの復帰: sigreturn()

// シグナルハンドラの実行終了後、libc が sigreturn() を呼び出す
// → sys_rt_sigreturn() → restore_sigcontext()
//   sigframe から保存されたすべてのレジスタを復元
//   → プロセスはシグナルによって中断された地点から実行を再開

sigframe はシグナルメカニズムの要諦です——カーネルはユーザースタック上に「スナップショット」を偽造し、ハンドラ実行後に sigreturn によってスナップショットを復元します。これにより、プロセスはハンドラの副作用を除き、何事もなかったかのように実行を継続します。

5. シグナルとシステムコール

シグナルによって中断されたシステムコールの 3 つの処理:
  SA_RESTART:   カーネルが自動的に再試行 (read/write/wait/... などの「低速」システムコールのみ)
  SA_RESTART なし: -EINTR を返し、ユーザー空間で再試行するかどうかを決定
  致命的シグナル: プロセスが終了し、戻らない
  
  ERESTARTSYS / ERESTARTNOINTR / ... :
    カーネル内部で使用される特殊なエラーコード。ユーザーモード復帰前に以下の変換が行われる:
      ERESTARTSYS   → SA_RESTART が設定されていれば再試行 / そうでなければ → EINTR
      ERESTARTNOHAND → ブロックされているがハンドラがない → 再試行
      ERESTART_RESTARTBLOCK → restart_block メカニズムが必要 (例: nanosleep)

SIGKILL と SIGSTOP の上書き不可能性

// kernel/signal.c
// SIGKILL (9) と SIGSTOP (19) は以下によって:
//   - ブロックできない (sigprocmask はこれらを無視する)
//   - 無視できない (SIG_IGN は無視される)
//   - ハンドラで捕捉できない (sa_handler は無視される)
//
// これは設計原則である: システムには常に最終手段として制御を回復する手段がある
// プロセスがすべてのシグナルをブロックしても、SIGKILL は依然として有効

// 例外: TASK_UNINTERRUPTible (D 状態) のプロセス
//   プロセスが D 状態(ディスク IO の待機など)でブロックされている場合、SIGKILL は即座に終了できない
//   IO が完了するまで待機 → プロセスが実行可能状態に戻る → シグナル配信が有効になる
//   TASK_KILLABLE (5.14+) はこれを改善: SIGKILL によってウェイクアップ可能

ptrace

ptrace は、トレーサープロセスがトレシープロセスの実行を制御することを可能にします。gdb(ブレークポイント/ステップ実行)、strace(システムコールのトレース)、seccomp user notification はすべて ptrace を基盤として構築されています。

コア操作

// kernel/ptrace.c
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

// 重要なリクエスト:
PTRACE_ATTACH / PTRACE_SEIZE  // 対象プロセスへのアタッチ
PTRACE_DETACH                 // デタッチ
PTRACE_SYSCALL                // 各システムコールのエントリ/エクジットで停止
PTRACE_SINGLESTEP             // 1命令ずつステップ実行
PTRACE_CONT                   // 実行を継続
PTRACE_PEEKDATA / PTRACE_POKEDATA  // トレシーのメモリを読み書き
PTRACE_GETREGS / PTRACE_SETREGS    // レジスタを読み書き
PTRACE_O_TRACESYSGOOD         // システムコール停止をマークして区別可能にする (シグナルのビット 7)
PTRACE_O_TRACEFORK / PTRACE_O_TRACEVFORK / PTRACE_O_TRACECLONE
PTRACE_O_TRACEEXIT
PTRACE_GET_SYSCALL_INFO       // 詳細なシステムコール情報を取得 (5.3+)

内部メカニズム

// アタッチフロー:
ptrace(PTRACE_ATTACH, pid, ...)ptrace_attach()
    → task->ptrace = PT_PTRACED を設定
    → task->parent = tracer を設定 (実際の親は real_parent に保存)
    → tracee に SIGSTOP を送信
    → tracee は TASK_TRACED 状態に入る
    → tracer は waitpid() によって停止通知を取得可能

// シグナルのインターセプト:
// tracee がシグナルを受信すると、カーネルは task->ptrace をチェック:
//   → PT_PTRACED: シグナルはまず tracer に送信 (tracer は修正または消費可能)
//   → tracer は waitpid() によってシグナル情報を取得
//   → tracer はシグナルの通過 (PTRACE_CONT) または無視を決定

// システムコールのトレース:
// PTRACE_SYSCALL → TIF_SYSCALL_TRACE を設定
// → システムコールエントリ: syscall_trace_enter() → ptrace_report_syscall()
//   → tracee は TASK_TRACED 状態になり、tracer は waitpid によって通知を取得
//   → tracer が復帰 → tracee はシステムコール本体を実行
// → システムコールエクジット: syscall_trace_exit() → 再度停止
//   → tracer はシステムコールの戻り値を確認可能

ptrace のアクセスモード

// PTRACE_PEEKDATA: /proc/<pid>/mem の低レベルインターフェースを介して実行
// 1ワードずつ読み取る (ptrace の伝統的な API、効率が低い)
// 
// より効率的な方法: /proc/<pid>/mem を直接読み書き (ptrace アタッチは不要!)
//   → process_vm_readv() / process_vm_writev()
//   → ただし、読み書きのみ可能で、実行の制御はできない

// カーネル実装: ptrace_access_vm() → get_user_pages_remote()
//   → ページテーブルを介して tracee の仮想アドレスを自アドレス空間にマップ
//   → 直接メモリアクセス (copy_from_user は不要)

ptrace と seccomp の排他制御

seccomp (SECCOMP_SET_MODE_FILTER):
  1つのプロセスは、ptrace されているか seccomp filter が設定されているかのいずれかであり、両方を併用することはできない
  理由: セキュリティ — seccomp の前提は「自分自身のシステムコールポリシーを制御している」こと
        ptrace されている場合、tracer は POKEDATA を通じて seccomp を回避可能

  SECCOMP_RET_USER_NOTIF (seccomp user notification):
    これは「seccomp フレームワーク内の ptrace 代替手段」
    → 監視プロセスがサンドボックスプロセスのシステムコールをチェック/承認可能
    → PT_PTRACED を設定しないため、seccomp フィルターと共存可能
    → ptrace より軽量: システムコールのみをインターセプトし、シグナルはインターセプトしない

デバッグと観測

# プロセスが現在ブロック中/保留中/無視中のシグナル
cat /proc/<pid>/status | grep -i sig
# SigBlk: ブロックされているシグナル
# SigIgn: 無視されているシグナル
# SigCgt: 捕捉されているシグナル (ハンドラあり)
# SigPnd: スレッドごとの保留中
# ShdPnd: スレッドグループ共有の保留中

# どのプロセスが最も多くのシグナルを保留しているか確認 (シグナルの洪水に襲われている可能性)
for pid in /proc/[0-9]*; do
    pend=$(awk '/SigPnd:/{print $2}' $pid/status 2>/dev/null)
    [ "$pend" != "0000000000000000" ] && echo "$pid: $pend"
done

# シグナルの配信を strace で確認
strace -e trace=signal -p <pid>

# シグナル送信時にカーネルトレースを表示
echo 1 > /sys/kernel/debug/tracing/events/signal/signal_generate/enable
echo 1 > /sys/kernel/debug/tracing/events/signal/signal_deliver/enable
cat /sys/kernel/debug/tracing/trace_pipe

参考と拡張

  • カーネルドキュメント⁠: Documentation/process/adding-syscalls.rst (シグナルとシステムコールの相互作用), man 7 signal, man 2 ptrace
  • LWN:
    • "A new API for signal handling" (lwn.net/Articles/414618/)
    • "Seccomp user notification" (lwn.net/Articles/785746/)
  • ソースファイル⁠:
    • kernel/signal.c — send_signal(), get_signal(), do_signal()
    • kernel/ptrace.c — ptrace_attach(), ptrace_resume()
    • arch/x86/kernel/signal.c — handle_signal(), setup_rt_frame()
    • arch/x86/kernel/ptrace.c — レジスタの読み書き

キーワード: sigaction, sigpending, SIGKILL, SA_RESTART, TIF_SIGPENDING, sigframe, rt_sigreturn, PTRACE_SYSCALL, TASK_TRACED, seccomp