Skip to content

Commit 1f2f7a5

Browse files
minto-daneCopilot
andcommitted
security: harden KPTI and close residual gaps
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6bc18b6 commit 1f2f7a5

10 files changed

Lines changed: 192 additions & 125 deletions

File tree

SECURITY_REVIEW.md

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ Mailbox は thread slot 単位。宛先 thread id 検証を行い、受信時に
238238
### 6.3 wait/reap
239239

240240
`wait` は parent-child 関係を前提に zombie を回収し、`WNOHANG` を実装。
241-
フェイルセーフとして 30秒 timeout が入る(POSIX完全互換ではない)
241+
ブロッキング待機時は無期限で待機し、回収時に子プロセスの user page table を破棄して frame 解放へ接続する
242242

243243
---
244244

@@ -255,37 +255,31 @@ Mailbox は thread slot 単位。宛先 thread id 検証を行い、受信時に
255255
- ELF 境界チェック強化
256256
- user stack NX + guard
257257
- FD owner 分離
258+
- SYSCALL stack pointer の per-CPU GS 化(SMP時の cross-core stack 汚染対策)
259+
- zombie 回収時の child page table 自動破棄
260+
- `RtSigaction` / `RtSigprocmask``ENOSYS` 明示化
261+
- カーネルスタック guard の未マップ化(fault-before-corruption)
262+
- IPC 宛先スロット世代検証(再利用スロット誤配送対策)
263+
- `wait` の無期限待機化(`WNOHANG` 以外)
258264

259265
### 7.2 残余リスク / 既知ギャップ
260266

261-
| ID | 重大度 | 内容 | 根拠 |
267+
| ID | 状態 | 修正内容 | 根拠 |
262268
|---|---|---|---|
263-
| R-01 | High (SMP時) / Low (現単一CPU想定) | `SYSCALL_KERNEL_RSP` がグローバルで、per-CPU state が syscall ASM で未活用。SMP有効時に cross-core stack 汚染リスク| `syscall/syscall_entry.rs`, `percpu.rs` |
264-
| R-02 | High (可用性) | zombie 回収時に page table / user frame 解放が自動連動せず、長時間運用でメモリリークにより OOM 化しうる| `task/process.rs::reap_zombie_child_process``paging::destroy_user_page_table` の非接続 |
265-
| R-03 | Medium | `RtSigaction`/`RtSigprocmask` SUCCESS スタブ。アプリは「設定できた」と誤認し得る| `syscall/mod.rs::dispatch` |
266-
| R-04 | Medium | カーネルスタック guard は未マップ型ではなくパターン検知型。検知は可能だが事前遮断ではない| `task/thread.rs`, `task/context.rs` |
267-
| R-05 | Low | IPC送信先存在確認と enqueue が原子的でなく、孤立メッセージが残る可能性| `syscall/ipc.rs` 内コメント |
268-
| R-06 | Low | `wait` 30秒 timeout を持つため POSIX の無期限待機と差異| `syscall/process.rs` |
269+
| R-01 | Closed | SYSCALL エントリのカーネルスタック取得を `gs:[offset]` に変更し、`IA32_KERNEL_GS_BASE`per-CPU state に接続| `syscall/syscall_entry.rs`, `percpu.rs` |
270+
| R-02 | Closed | zombie 回収で child page table `destroy_user_page_table` へ接続し、回収時に frame 解放| `task/process.rs`, `mem/paging.rs` |
271+
| R-03 | Closed | `RtSigaction`/`RtSigprocmask` `SUCCESS` スタブから `ENOSYS` に変更| `syscall/mod.rs::dispatch` |
272+
| R-04 | Closed | カーネルスタック guard を未マップページ化し、コンテキスト切替時に guard 未マップを検証| `task/thread.rs`, `task/context.rs` |
273+
| R-05 | Closed | IPC メッセージへ宛先スロット世代を付与し、受信時一致検証で再利用スロット誤配送を遮断| `syscall/ipc.rs`, `task/thread.rs` |
274+
| R-06 | Closed | `wait` 30秒 timeout を撤廃し、`WNOHANG` 以外は無期限待機へ変更| `syscall/process.rs` |
269275

270276
---
271277

272-
## 8. 優先改善提案(実装順
278+
## 8. 継続改善提案(運用品質
273279

274-
### P0(先行推奨)
275-
276-
1. `wait/reap` で child の page table 破棄 (`destroy_user_page_table`) を接続
277-
2. SYSCALL stack pointer を GS/per-CPU 経由へ完全移行(グローバル `SYSCALL_KERNEL_RSP` 脱却)
278-
3. signal syscall を `ENOSYS` 明示または最小実装へ変更(偽SUCCESSを避ける)
279-
280-
### P1
281-
282-
4. カーネルスタックを未マップ guard page 化(検知型 -> fault-before-corruption)
283-
5. IPC 宛先検証と enqueue の原子化(スロット世代番号など)
284-
285-
### P2
286-
287-
6. KPTI の共有範囲さらに最小化
288-
7. 追加動的検証(fuzz, stress, long-run leak test)
280+
1. KPTI の共有マッピング範囲を実測に基づいてさらに縮小(機能維持を確認しながら段階適用)
281+
2. syscall / IPC / wait-reap のストレス試験を継続運用に組み込む
282+
3. SMP 構成での per-CPU GS 初期化と SYSCALL 経路の長時間回帰試験を追加
289283

290284
---
291285

src/core/init/mod.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,5 @@ pub fn kinit(boot_info: &'static BootInfo) -> Result<&'static [MemoryRegion]> {
5757
// SYSCALL/SYSRET 命令サポートを初期化
5858
crate::syscall::syscall_entry::init_syscall();
5959

60-
// Set IA32_KERNEL_GS_BASE to kernel per-cpu base (placeholder: SYSCALL_KERNEL_STACK)
61-
unsafe {
62-
let kgs_base =
63-
core::ptr::addr_of!(crate::syscall::syscall_entry::SYSCALL_KERNEL_STACK) as u64;
64-
let lo = kgs_base as u32;
65-
let hi = (kgs_base >> 32) as u32;
66-
core::arch::asm!("wrmsr", in("ecx") 0xC000_0102u32, in("eax") lo, in("edx") hi, options(nomem, nostack));
67-
crate::info!("IA32_KERNEL_GS_BASE set to {:#x}", kgs_base);
68-
}
69-
7060
Ok(memory_map)
7161
}

src/core/percpu.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use core::sync::atomic::{AtomicU64, Ordering};
77
use x86_64::registers::control::Cr3;
88

99
const MAX_CPUS: usize = 64;
10+
const IA32_KERNEL_GS_BASE: u32 = 0xC000_0102;
11+
pub const GS_SYSCALL_KERNEL_RSP_OFFSET: usize = 8;
1012

1113
#[repr(C)]
1214
struct PerCpuState {
@@ -32,6 +34,19 @@ fn state_for_current_cpu() -> &'static PerCpuState {
3234
&CPU_STATES[current_cpu_id()]
3335
}
3436

37+
#[inline]
38+
unsafe fn write_kernel_gs_base(base: u64) {
39+
let lo = base as u32;
40+
let hi = (base >> 32) as u32;
41+
asm!(
42+
"wrmsr",
43+
in("ecx") IA32_KERNEL_GS_BASE,
44+
in("eax") lo,
45+
in("edx") hi,
46+
options(nomem, nostack)
47+
);
48+
}
49+
3550
#[inline]
3651
pub fn current_cpu_id() -> usize {
3752
(local_apic_id() as usize) % MAX_CPUS
@@ -66,6 +81,14 @@ pub fn init_boot_cpu(syscall_kernel_rsp: u64) {
6681
.syscall_kernel_rsp
6782
.store(syscall_kernel_rsp, Ordering::SeqCst);
6883
state.current_thread_id.store(0, Ordering::SeqCst);
84+
install_current_cpu_gs_base();
85+
}
86+
87+
pub fn install_current_cpu_gs_base() {
88+
let state = state_for_current_cpu() as *const PerCpuState as u64;
89+
unsafe {
90+
write_kernel_gs_base(state);
91+
}
6992
}
7093

7194
pub fn kernel_cr3() -> u64 {

src/core/syscall/ipc.rs

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const MAX_MSG_SIZE: usize = 256;
1010
pub struct Message {
1111
from: u64,
1212
to: u64,
13+
to_slot: u16,
14+
to_generation: u64,
1315
len: usize,
1416
data: [u8; MAX_MSG_SIZE],
1517
}
@@ -19,6 +21,8 @@ impl Message {
1921
Self {
2022
from: 0,
2123
to: 0,
24+
to_slot: 0,
25+
to_generation: 0,
2226
len: 0,
2327
data: [0; MAX_MSG_SIZE],
2428
}
@@ -88,19 +92,20 @@ pub fn send(dest_thread_id: u64, buf_ptr: u64, len: u64) -> u64 {
8892
None => return EINVAL,
8993
};
9094

91-
let idx = match crate::task::thread_slot_index_by_u64(dest_thread_id) {
92-
Some(i) => i,
93-
None => return EINVAL,
94-
};
95+
let (idx, dest_generation) =
96+
match crate::task::thread_slot_index_and_generation_by_u64(dest_thread_id) {
97+
Some(v) => v,
98+
None => return EINVAL,
99+
};
95100

96-
if !crate::task::thread_id_exists(dest_thread_id) {
101+
if idx >= MAX_THREADS || idx > (u16::MAX as usize) {
97102
return EINVAL;
98103
}
99104

100-
// 送信先スレッドが実際に存在するか確認 (#14: ゴーストメッセージ注入防止)
101-
// NOTE: このチェックと後続の mailbox enqueue は別ロックであり原子的ではない
102-
// したがって、チェック後に送信先が終了すると孤立メッセージが残る可能性はある
103-
// メッセージに `to` を保持し、受信側で宛先一致を確認することで誤配送を防ぐ
105+
// NOTE:
106+
// - 宛先スロットに加えて世代番号をメッセージへ埋め込む
107+
// - これにより、送信先終了後に同一スロットへ別スレッドが再利用されても誤配送されない
108+
// - 送信時点と受信時点で世代不一致なら古いメッセージとして破棄される
104109

105110
// データをユーザー空間からコピー
106111
let mut data = [0u8; MAX_MSG_SIZE];
@@ -118,6 +123,8 @@ pub fn send(dest_thread_id: u64, buf_ptr: u64, len: u64) -> u64 {
118123
let msg = Message {
119124
from: sender,
120125
to: dest_thread_id,
126+
to_slot: idx as u16,
127+
to_generation: dest_generation,
121128
len,
122129
data,
123130
};
@@ -140,15 +147,26 @@ pub fn recv(buf_ptr: u64, max_len: u64) -> u64 {
140147
None => return EINVAL,
141148
};
142149

143-
let idx = match crate::task::thread_slot_index_by_u64(receiver) {
144-
Some(i) => i,
145-
None => return EINVAL,
146-
};
150+
let (idx, receiver_generation) =
151+
match crate::task::thread_slot_index_and_generation_by_u64(receiver) {
152+
Some(v) => v,
153+
None => return EINVAL,
154+
};
155+
156+
if idx >= MAX_THREADS || idx > (u16::MAX as usize) {
157+
return EINVAL;
158+
}
147159

148160
let mut boxes = MAILBOXES.lock();
149161
let msg = loop {
150162
match boxes[idx].pop() {
151-
Some(msg) if msg.to == receiver => break msg,
163+
Some(msg)
164+
if msg.to == receiver
165+
&& msg.to_slot == idx as u16
166+
&& msg.to_generation == receiver_generation =>
167+
{
168+
break msg
169+
}
152170
Some(_) => continue, // 既に終了した別スレッド宛の古いメッセージを破棄
153171
None => return EAGAIN,
154172
}

src/core/syscall/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ pub fn dispatch(num: u64, arg0: u64, arg1: u64, arg2: u64, arg3: u64, arg4: u64)
138138
x if x == SyscallNumber::Mmap as u64 => process::mmap(arg0, arg1, arg2, arg3, arg4),
139139
x if x == SyscallNumber::Munmap as u64 => process::munmap(arg0, arg1),
140140
x if x == SyscallNumber::Brk as u64 => process::brk(arg0),
141-
x if x == SyscallNumber::RtSigaction as u64 => SUCCESS, // スタブ
142-
x if x == SyscallNumber::RtSigprocmask as u64 => SUCCESS, // スタブ
141+
x if x == SyscallNumber::RtSigaction as u64 => ENOSYS,
142+
x if x == SyscallNumber::RtSigprocmask as u64 => ENOSYS,
143143
x if x == SyscallNumber::GetPid as u64 => process::getpid(),
144144
x if x == SyscallNumber::Clone as u64 => process::fork(),
145145
x if x == SyscallNumber::Fork as u64 => process::fork(),
@@ -307,15 +307,15 @@ extern "C" fn syscall_handler_rust(
307307
arg4: u64,
308308
) -> u64 {
309309
let current_tid = crate::task::current_thread_id();
310+
let prev_cr3 = syscall_entry::switch_to_kernel_page_table();
310311
if let Some(tid) = current_tid {
311312
crate::task::with_thread_mut(tid, |t| t.set_in_syscall(true));
312313
}
313-
let prev_cr3 = syscall_entry::switch_to_kernel_page_table();
314314
let ret = dispatch(num, arg0, arg1, arg2, arg3, arg4);
315-
syscall_entry::restore_page_table(prev_cr3);
316315
if let Some(tid) = current_tid {
317316
crate::task::with_thread_mut(tid, |t| t.set_in_syscall(false));
318317
}
318+
syscall_entry::restore_page_table(prev_cr3);
319319
ret
320320
}
321321

src/core/syscall/process.rs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ const ECHILD: u64 = (-10i64) as u64;
1212
const ETIMEDOUT: u64 = (-110i64) as u64;
1313
/// PIT割り込み周期 (10ms)
1414
const TICK_MS: u64 = 10;
15-
/// wait のフェイルセーフ上限 (30秒)
16-
const WAIT_TIMEOUT_TICKS: u64 = 30_000 / TICK_MS;
1715
use crate::task::{current_thread_id, exit_current_task};
1816

1917
#[derive(Clone, Copy)]
@@ -391,11 +389,6 @@ pub fn wait(_pid: u64, status_ptr: u64, options: u64) -> u64 {
391389
};
392390

393391
// POSIX互換の待機: ゾンビを回収、存在しなければブロックまたはWNOHANGで0
394-
let wait_deadline = if options & WNOHANG == 0 {
395-
Some(crate::syscall::time::get_ticks().saturating_add(WAIT_TIMEOUT_TICKS))
396-
} else {
397-
None
398-
};
399392
loop {
400393
if let Some((reaped_pid, exit_code)) =
401394
crate::task::reap_zombie_child_process(current_pid, target_pid)
@@ -417,10 +410,6 @@ pub fn wait(_pid: u64, status_ptr: u64, options: u64) -> u64 {
417410
return 0;
418411
}
419412

420-
if wait_deadline.is_some_and(|deadline| crate::syscall::time::get_ticks() >= deadline) {
421-
return ETIMEDOUT;
422-
}
423-
424413
crate::task::yield_now();
425414
}
426415
}

src/core/syscall/syscall_entry.rs

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,6 @@
1010
//! R11 = ユーザーの RFLAGS (SYSCALL が自動保存)
1111
//! RSP = まだユーザースタック
1212
13-
use core::sync::atomic::{AtomicU64, Ordering};
14-
15-
/// 現在のスレッドのカーネルスタックトップ (SYSCALL 時に切り替える)
16-
/// コンテキストスイッチ時に更新される
17-
pub static SYSCALL_KERNEL_RSP: AtomicU64 = AtomicU64::new(0);
18-
19-
/// SYSCALL 用カーネルスタック (初回スイッチまたはスレッド切り替え前に使用)
20-
#[repr(align(16))]
21-
pub struct SyscallStack([u8; 4096 * 8]);
22-
23-
/// SYSCALL 用カーネルスタックの実体
24-
pub static mut SYSCALL_KERNEL_STACK: SyscallStack = SyscallStack([0; 4096 * 8]);
25-
2613
/// SYSCALL/SYSRET に必要な MSR を初期化する
2714
///
2815
/// カーネル GDT 構成 (x86_64-unknown-uefi / MS ABI):
@@ -68,13 +55,11 @@ pub fn init_syscall() {
6855
write_msr(IA32_FMASK, fmask_val);
6956
}
7057

71-
// 初期カーネルスタックを設定
72-
let kstack_top = unsafe {
73-
let base = core::ptr::addr_of!(SYSCALL_KERNEL_STACK) as u64;
74-
base + 4096 * 8
75-
};
76-
77-
SYSCALL_KERNEL_RSP.store(kstack_top, Ordering::SeqCst);
58+
// 初期カーネルスタックは現在のRSPを使用し、後続のコンテキストスイッチで更新する
59+
let kstack_top: u64;
60+
unsafe {
61+
core::arch::asm!("mov {}, rsp", out(reg) kstack_top, options(nomem, nostack, preserves_flags));
62+
}
7863
crate::percpu::init_boot_cpu(kstack_top);
7964

8065
crate::info!("SYSCALL/SYSRET initialized: LSTAR={:#x}", lstar_val);
@@ -83,7 +68,6 @@ pub fn init_syscall() {
8368
/// SYSCALL カーネルスタックを更新する (コンテキストスイッチ時に呼ぶ)
8469
pub fn update_kernel_rsp(rsp: u64) {
8570
// SeqCst を使用してメモリ順序を保証する (MED-05)
86-
SYSCALL_KERNEL_RSP.store(rsp, Ordering::SeqCst);
8771
crate::percpu::set_syscall_kernel_rsp(rsp);
8872
}
8973

@@ -179,7 +163,7 @@ pub unsafe extern "C" fn syscall_entry() {
179163
"or rdx, rax",
180164

181165
// カーネルスタックに切り替え
182-
"mov rsp, [{kernel_rsp}]",
166+
"mov rsp, qword ptr gs:[{sys_rsp_off}]",
183167
// ユーザーFSベースを保存
184168
"push rdx",
185169

@@ -292,10 +276,10 @@ pub unsafe extern "C" fn syscall_entry() {
292276

293277
// 非正規RSP検出: カーネルスタックに戻してプロセスを終了
294278
"2:",
295-
"mov rsp, [{kernel_rsp}]",
279+
"mov rsp, qword ptr gs:[{sys_rsp_off}]",
296280
"call {kill_fn}",
297281

298-
kernel_rsp = sym SYSCALL_KERNEL_RSP,
282+
sys_rsp_off = const crate::percpu::GS_SYSCALL_KERNEL_RSP_OFFSET,
299283
save_ctx_fn = sym super::save_user_context_for_fork,
300284
dispatch = sym super::syscall_dispatch_sysv,
301285
kill_fn = sym kill_non_canonical_rsp,

src/core/task/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub use scheduler::{
2424
pub use thread::{
2525
add_thread, count_threads_by_state, current_thread_id, for_each_thread, peek_next_thread,
2626
remove_thread, set_current_thread, thread_count, thread_id_exists, thread_slot_index,
27+
thread_slot_index_and_generation, thread_slot_index_and_generation_by_u64,
2728
thread_slot_index_by_u64, with_thread, with_thread_mut, Thread, ThreadQueue,
2829
};
2930
pub use usermode::{jump_to_usermode, jump_to_usermode_fork_child};

0 commit comments

Comments
 (0)