Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

- **vz-smoke.sh 的 pkill 治標可升級為斷言**:helper stdin-EOF 自我了結(vz.rs + vz-helper/main.swift)合併後,`scripts/vz-smoke.sh` 收尾的 `pkill -f chefer-vz-helper`(目前在主 checkout 未 commit 的變更中)可改成「等 ~10s 斷言無 chefer-vz-helper 殘留」,讓實機一鍵驗證直接覆蓋這個修復。
- **runtime 單發 SIGINT 下 helper 收攤有 ~5s 延遲**:runtime 的 ctrlc handler 等 5 秒才 `exit(130)`,stdin EOF 要到行程死亡才發生(實測 SIGINT→helper 收攤約 6s;SIGTERM/SIGKILL 即時)。若要即時收攤,可讓 vz 後端把 helper stdin 寫端交給訊號處理路徑、收訊號時主動關閉。屬優化,非正確性問題。
- **Windows whp-helper 疑有同款孤兒化問題**:`crates/vmm-backend/src/whp.rs` spawn `chefer-whp-helper` 時無 Job Object、無 stdin liveness——對 runtime 單殺(`taskkill /PID`,非 console Ctrl+C)時 helper/VM 推測會殘留(程式碼層面確認無任何防護;未在實機重現)。建議用 Job Object(`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`)或比照 vz 的 stdin-EOF 做法
- **whp 防孤兒修復待實機驗證**:Job Object(KILL_ON_JOB_CLOSE)+ helper stdin-EOF 自我了結(`crates/vmm-backend/src/whp.rs` + `crates/whp-helper/src/main.rs`)已完成、機制各有 CI 單元測試,但「單殺 runtime 後 helper/VM 不殘留」的端到端行為需實體 Windows(WHP)驗證:以 `CHEFER_BACKEND=whp` 跑一個 bundle → `taskkill /F /PID <runtime pid>`(TerminateProcess,孤兒化的可靠重現法;原始問題本身也尚未在實機重現過)→ 確認 `chefer-whp-helper` 行程數秒內消失、無殘留。通過後刪除本項
8 changes: 7 additions & 1 deletion crates/vmm-backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ tar = "0.4.46"

[target."cfg(windows)".dependencies]
tempfile = "3.27.0"
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_LibraryLoader", "Win32_System_Threading"] }
windows-sys = { version = "0.61", features = [
"Win32_Foundation",
"Win32_System_LibraryLoader",
"Win32_System_Threading",
# WHP helper 防孤兒:KILL_ON_JOB_CLOSE Job Object(whp.rs,DESIGN §6「Helper 生命週期」)。
"Win32_System_JobObjects",
] }

[dev-dependencies]
serde_json = "1.0.145"
Expand Down
106 changes: 105 additions & 1 deletion crates/vmm-backend/src/whp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@
use std::ffi::c_void;
use std::io::{BufRead, BufReader};
use std::mem::size_of;
use std::os::windows::io::AsRawHandle;
use std::process::{Command, Stdio};

use anyhow::{Context, Result, bail};
use chefer_bundle::PortProto;

use windows_sys::Win32::Foundation::FreeLibrary;
use windows_sys::Win32::Foundation::{CloseHandle, FreeLibrary, HANDLE};
use windows_sys::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
SetInformationJobObject,
};
use windows_sys::Win32::System::LibraryLoader::{
GetProcAddress, LOAD_LIBRARY_SEARCH_SYSTEM32, LoadLibraryExA,
};
Expand Down Expand Up @@ -116,6 +122,7 @@ impl ExecBackend for WhpBackend {

let mut child = Command::new(&invocation.helper)
.args(invocation.args())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.with_context(|| {
Expand All @@ -125,6 +132,28 @@ impl ExecBackend for WhpBackend {
)
})?;

// 防孤兒(DESIGN §6 whp「Helper 生命週期」):runtime 的 Ctrl+C 處理仰賴 helper
// 同 console 收到同一個 console 事件,但對 runtime 單殺(taskkill /PID,非 console
// Ctrl+C)時 helper 收不到任何訊號,micro-VM 會殘留。雙保險:
// ① Job Object(KILL_ON_JOB_CLOSE):job handle 故意洩漏、隨本行程存亡——runtime
// 無論怎麼死(taskkill /F、當機、正常結束)OS 都會關閉 handle → job 關閉 →
// helper 連同 VM 被系統終結。spawn 與掛入之間的微小空窗與掛入失敗由 ② 後援。
if let Err(e) = assign_to_kill_on_close_job(&child) {
eprintln!(
"[chefer] warning: failed to put the WHP helper in a kill-on-close job: {e}; \
relying on the helper's stdin-EOF self-termination"
);
}

// ② stdin liveness(與 vz 同契約):helper 讀到 stdin EOF 即自我了結。寫端由本
// 行程握住不寫——WHP 的 guest console 無輸入路徑(serial 僅 TX),無 vz 的
// terminal stdin 泵送需求——mem::forget 讓 fd 隨行程存亡。
let helper_stdin = child
.stdin
.take()
.expect("child stdin was requested as piped");
std::mem::forget(helper_stdin);

let stdout = child
.stdout
.take()
Expand Down Expand Up @@ -154,6 +183,39 @@ impl ExecBackend for WhpBackend {
}
}

// ---------------------------------------------------------------------------
// Helper 防孤兒(Job Object)
// ---------------------------------------------------------------------------

/// 把 child 掛進 `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` 的 Job Object,回傳 job handle
///(防孤兒 ①,DESIGN §6 whp「Helper 生命週期」)。呼叫端**故意洩漏** handle(不
/// CloseHandle)——handle 隨本行程存亡,行程死亡(任何方式)時 OS 關閉它 → job 關閉
/// → child 連同其中的 micro-VM 被系統終結。Windows 8+ 支援巢狀 job:本行程已在別的
/// job(如 CI runner)也掛得進去。
fn assign_to_kill_on_close_job(child: &std::process::Child) -> std::io::Result<HANDLE> {
unsafe {
let job = CreateJobObjectW(std::ptr::null(), std::ptr::null());
if job.is_null() {
return Err(std::io::Error::last_os_error());
}
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
let configured = SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
(&raw const info).cast(),
size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
) != 0
&& AssignProcessToJobObject(job, child.as_raw_handle() as HANDLE) != 0;
if !configured {
let err = std::io::Error::last_os_error();
CloseHandle(job);
return Err(err);
}
Ok(job)
}
}

// ---------------------------------------------------------------------------
// Host capability probing
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -363,4 +425,46 @@ mod tests {

assert!(reason.contains("missing agents/chefer-whp-helper-x86_64.exe"));
}

/// 鎖住防孤兒 ① 的核心語意(DESIGN §6 whp「Helper 生命週期」):關掉唯一的
/// job handle → job 內的行程被系統終結(KILL_ON_JOB_CLOSE)。真 WHP VM 情境
///(runtime 被 taskkill)只能實機驗證;此測試以長命 dummy child 在 CI 的
/// Windows runner 上鎖住機制本身。
#[test]
fn kill_on_close_job_terminates_child_when_handle_closes() {
use std::time::Duration;

// 長命 child(約 60 秒自我了結——測試失敗時不殘留)。
let mut child = Command::new("ping")
.args(["-n", "60", "127.0.0.1"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn the dummy child (ping)");

let job = assign_to_kill_on_close_job(&child).expect("failed to create/assign the job");
assert!(
child.try_wait().expect("try_wait failed").is_none(),
"the child died right after being assigned to the job"
);

// 關掉唯一的 handle:模擬 runtime 行程死亡時 OS 的行為。
unsafe { CloseHandle(job) };

let mut exited = false;
for _ in 0..100 {
if child.try_wait().expect("try_wait failed").is_some() {
exited = true;
break;
}
std::thread::sleep(Duration::from_millis(100));
}
if !exited {
let _ = child.kill();
}
assert!(
exited,
"the child survived closing the job handle; KILL_ON_JOB_CLOSE did not take effect"
);
}
}
78 changes: 78 additions & 0 deletions crates/whp-helper/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,40 @@ fn run(args: impl IntoIterator<Item = String>) -> Result<(), String> {
}

let request = HelperRequest::parse(args)?;

// stdin liveness(防孤兒 ②,與 vz-helper 同契約;DESIGN §6 whp「Helper 生命週期」):
// chefer-runtime 以 piped stdin spawn 本程式並讓寫端 fd 隨其行程存亡——runtime 無論
// 怎麼死(taskkill、當機、正常結束)OS 都會關 fd,這裡讀到 EOF 即自我了結(VM 在
// 本行程內,exit 即拆)。WHP 的 guest console(ttyS0)無輸入路徑(serial.rs 僅 TX),
// 讀到的資料直接丟棄;未來若接 console 輸入,這條執行緒就是轉發點。僅 boot 模式
// 安裝——--preflight/--gui-selftest 由 `.output()` 驅動(stdin 立即 EOF),裝了會
// 誤殺;手動在終端跑 helper 時 stdin 是 console、不會 EOF,行為不變。cfg(not(test)):
// 單元測試走進 run() 時不得讀走測試行程的 stdin(cargo test 下常已關閉→立即 EOF
// →誤殺整個測試行程)。
#[cfg(not(test))]
std::thread::spawn(|| {
drain_until_eof(std::io::stdin().lock());
eprintln!("chefer-whp-helper: stdin closed (chefer-runtime is gone); shutting down");
std::process::exit(0);
});

run_boot(request)
}

/// 讀 `reader` 直到 EOF,資料一律丟棄;`Interrupted` 重試,其他讀取錯誤視同 EOF
///(stdin 壞掉時同樣不能再當存活訊號)。回傳=寫端已全數消失。
fn drain_until_eof(mut reader: impl std::io::Read) {
let mut buf = [0u8; 1024];
loop {
match reader.read(&mut buf) {
Ok(0) => return,
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(_) => return,
}
}
}

fn print_help() {
println!(
"chefer-whp-helper\n\
Expand Down Expand Up @@ -3283,4 +3314,51 @@ mod tests {
let err = run_preflight(args).unwrap_err();
assert!(err.contains("--cpus requires a value"));
}

// --- stdin liveness(drain_until_eof,DESIGN §6 whp「Helper 生命週期」)---

/// 先 Interrupted、再給資料、最後 EOF 的 reader——鎖住「Interrupted 重試、
/// 資料丟棄、EOF 才返回」的契約。
struct FlakyReader {
interrupts_left: u32,
data_left: usize,
}

impl std::io::Read for FlakyReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.interrupts_left > 0 {
self.interrupts_left -= 1;
return Err(std::io::Error::from(std::io::ErrorKind::Interrupted));
}
if self.data_left == 0 {
return Ok(0); // EOF
}
let n = self.data_left.min(buf.len());
self.data_left -= n;
Ok(n)
}
}

#[test]
fn drain_until_eof_retries_interrupted_and_returns_on_eof() {
// 返回本身就是斷言:Interrupted 沒把它打斷、資料沒被當 EOF 提早返回、
// EOF 後不再讀(卡住則測試逾時失敗)。
drain_until_eof(FlakyReader {
interrupts_left: 3,
data_left: 8192,
});
drain_until_eof(std::io::empty()); // 空輸入:立即 EOF
}

#[test]
fn drain_until_eof_treats_read_errors_as_eof() {
struct BrokenReader;
impl std::io::Read for BrokenReader {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe))
}
}
// 讀取錯誤視同 EOF:返回而非無窮重試。
drain_until_eof(BrokenReader);
}
}
1 change: 1 addition & 0 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ pub fn run_app(ctx: &AppRunContext) -> anyhow::Result<i32>; // 取第一個 Avai
7. **免 WSL 的 `whp` 後端**:以 **Windows Hypervisor Platform(WHP)** 開機 bundle 內附的 Linux micro-VM appliance(與 macOS `vz` 共用同一 kernel/initramfs/guest-agent),作為 `wsl2` 的替代後端,移除對 WSL2 的依賴(仍需硬體虛擬化 + WHP 功能)。對完全無虛擬化的機器,另可選擇性 bundle 軟體模擬(QEMU/TCG)作為最終備援(可跑但慢)。backend 抽象(`ExecBackend`)已納入 `whp`,Windows 後端排序為 `wsl2` → `whp`;`whp` 以 `WinHvPlatform.dll` + `WHvGetCapability(HypervisorPresent)` 做 host preflight,並在有 bundle context 時檢查 `vm/chefer-vmlinuz-<arch>`、`vm/chefer-initramfs-<arch>`、`agents/chefer-whp-helper-<arch>.exe` 是否存在——三者皆備且 hypervisor 可用時回傳 `Available`,由 runtime 的 `run()` spawn helper 執行完整開機,stdout 逐行解析 `CHEFER_GUEST_IP=<ipv4>` / `CHEFER_GUEST_EXIT=<code>` 標記。helper CLI contract:`chefer-whp-helper --kernel <p> --initramfs <p> --cmdline <s> --bundle-dir <p> --data-dir <p> --cpus <n> --memory-mib <n> [--timeout <secs>] [--forward-tcp <host:guest>]... [--forward-udp <host:guest>]...`(`--forward-tcp`/`--forward-udp` 可重複,宣告 host→guest TCP/UDP 埠轉發;見下 virtio-net 說明,WHP 的埠轉發 listener 由 helper 自身持有)。
**WHP 專屬 kernel 參數(已實測確認)**:`nolapic`(WHP LAPIC emulation 不完整,改由 host 直接注入 timer interrupt)、`lpj=1000000`(跳過 calibration busy-wait)、`notsc clocksource=jiffies`(TSC 在 minimal VM 不可靠)。此外 initramfs 內的 init 必須為 **非 PIE 靜態 ELF**(static non-PIE,`ET_EXEC`;PIE 的 `ET_DYN` 在無 dynamic linker 的 minimal VM 會 segfault),且 initramfs 必須包含 `/dev/console`(char device major=5, minor=1)供 kernel 開啟 init 的 stdio fd 0/1/2。
**Guest→host exit code 通道(`/dev/kmsg`)**:guest init 結束前將 `CHEFER_GUEST_EXIT=<code>` 寫入 **`/dev/kmsg`**(非 stdout),因 user-space 寫 `/dev/console`(tty 路徑)需 serial IRQ4 驅動 TX,而 WHP 的最小 8250 模擬不含 IRQ 觸發——`/dev/kmsg` 走 kernel printk polled I/O 路徑,可直接出現在 serial console。
**Helper 生命週期(防孤兒:Job Object + stdin liveness)**:runtime 的 Ctrl+C 處理仰賴「helper 與 runtime 同 console、console 事件同時送達」,但對 runtime **單殺**(`taskkill /PID <runtime>`,非 console Ctrl+C)時 helper 收不到任何訊號——與 vz 實機發現的孤兒化同款問題(見 vz 節),helper/micro-VM 會殘留繼續吃 CPU。修法雙保險:① **Job Object**——whp.rs spawn helper 後立刻把它掛進 `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` 的 Job Object 並**故意洩漏 job handle**(不 CloseHandle);handle 隨 runtime 行程存亡,runtime 無論怎麼死(taskkill /F、當機、正常結束)OS 都會關閉 handle → job 關閉 → helper 連同 VM 被系統終結(Windows 8+ 支援巢狀 job,runtime 本身已在別的 job(如 CI runner)也能掛;掛入失敗僅警告不阻斷,由 ② 後援)。② **stdin liveness(與 vz 同契約)**——whp.rs 以 `Stdio::piped()` spawn helper 並讓 stdin 寫端 fd 隨行程存亡(`mem::forget`);helper 端 boot 模式由專屬執行緒讀 stdin,讀到 **EOF 即自我了結**(VM 在 helper 行程內,exit 即拆)。與 vz 的差異:WHP 的 guest console(ttyS0)**沒有輸入路徑**(serial 模擬僅 TX),故無 terminal stdin 泵送、讀到的資料一律丟棄;若未來接上 console 輸入,這條執行緒就是轉發點。`--preflight`/`--gui-selftest` 模式**不**安裝 stdin 監看(它們由 `.output()` 驅動、stdin 立即 EOF,裝了會誤殺)。兩機制皆不觸發 vdb data 回寫(同 M3 既有限制:僅 guest 乾淨關機回寫)。**狀態**:程式碼完成;Job Object 的 kill-on-close 語意與 helper 的 EOF 偵測純邏輯由 CI 單元測試鎖住(前者僅 Windows runner 執行);真 WHP VM 情境的孤兒化重現與修復驗證同「驗證邊界」慣例,待實機。
**`--preflight` 模式**(`chefer-whp-helper --preflight [--cpus <n>]`):動態載入 `WinHvPlatform.dll` → 走一輪 WHP device model 基礎生命週期,不開機、不需 appliance 或 bundle 路徑:
1. partition 建立與設定:`WHvCreatePartition` → `WHvSetPartitionProperty(ProcessorCount)` → `WHvSetupPartition`;
2. GPA 記憶體映射:`VirtualAlloc` 配置一頁(4 KiB)零值記憶體 → `WHvMapGpaRange` 映射到 GPA 0(驗證 host→guest 記憶體管理路徑);
Expand Down
Loading