Skip to content
Closed
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
3 changes: 3 additions & 0 deletions rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ ordinary-spawn retry after terminating the original. This logged fallback uses
best-effort Job assignment: early descendants can escape, and assignment failure
leaves only direct-child cleanup, without crash-safe SDK-host-exit ownership.

Descendants explicitly launched with `CREATE_BREAKAWAY_FROM_JOB` can leave the
SDK-owned Job and survive its cleanup.

**`ClientOptions`:**

| Field | Type | Description |
Expand Down
11 changes: 7 additions & 4 deletions rust/src/process_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ mod platform {
CreateToolhelp32Snapshot, TH32CS_SNAPTHREAD, THREADENTRY32, Thread32First, Thread32Next,
};
use windows_sys::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation,
SetInformationJobObject, TerminateJobObject,
AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_BREAKAWAY_OK,
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JobObjectExtendedLimitInformation, SetInformationJobObject, TerminateJobObject,
};
use windows_sys::Win32::System::Threading::{
CREATE_NO_WINDOW, CREATE_SUSPENDED, GetCurrentProcess, GetProcessIdOfThread, OpenThread,
Expand Down Expand Up @@ -251,7 +251,10 @@ mod platform {
let job = OwnedHandle(raw_job);

let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
// Permit tool launches with CREATE_BREAKAWAY_FROM_JOB without silently
// letting ordinary children escape.
limits.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_BREAKAWAY_OK;
Comment thread
dibarbet marked this conversation as resolved.
// SAFETY: `limits` has the layout required by the selected info class.
if unsafe {
SetInformationJobObject(
Expand Down
53 changes: 52 additions & 1 deletion rust/src/process_tree/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ use windows_sys::Win32::Security::{
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, TH32CS_SNAPTHREAD, THREADENTRY32, Thread32First, Thread32Next,
};
use windows_sys::Win32::System::JobObjects::IsProcessInJob;
use windows_sys::Win32::System::JobObjects::{
IsProcessInJob, JOB_OBJECT_LIMIT_BREAKAWAY_OK, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JobObjectExtendedLimitInformation, QueryInformationJobObject,
};
use windows_sys::Win32::System::Threading::{
GetCurrentProcess, GetCurrentProcessId, GetCurrentThread, GetCurrentThreadId, OpenProcess,
OpenProcessToken, OpenThread, OpenThreadToken, PROCESS_SYNCHRONIZE, SetThreadToken,
Expand Down Expand Up @@ -778,6 +782,53 @@ async fn initial_thread_executes_only_after_private_job_assignment() {
drop(tree);
}

#[tokio::test]
async fn private_job_allows_only_explicit_breakaway() {
let _reset = ResetHooks;
BEFORE_RESUME.with_borrow_mut(|hook| {
*hook = Some(Box::new(|_, job| {
let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
// SAFETY: the Job remains owned and `limits` matches the requested information class.
let queried = unsafe {
QueryInformationJobObject(
job,
JobObjectExtendedLimitInformation,
ptr::from_mut(&mut limits).cast(),
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
ptr::null_mut(),
)
};
assert_ne!(queried, 0, "{}", io::Error::last_os_error());
let flags = limits.BasicLimitInformation.LimitFlags;
let required = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_BREAKAWAY_OK;
assert_eq!(
flags & required,
required,
"private Job must retain kill-on-close and permit explicit breakaway"
);
assert_eq!(
flags & JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK,
0,
"ordinary descendants must not silently escape the private Job"
);
Ok(())
}));
});
let mut command = command();
command.args(["exit", "/b", "0"]);
let (mut child, tree) = super::spawn(&mut command).expect("spawn owned child");
assert!(
BEFORE_RESUME.with_borrow(Option::is_none),
"job policy must be checked before resume"
);
let status = tokio::time::timeout(Duration::from_secs(5), child.wait())
.await
.expect("child must exit")
.expect("reap child");
drop(tree);
assert!(status.success());
}

#[tokio::test]
async fn lookup_failures_reap_suspended_root_and_release_pss_resources() {
let _reset = ResetHooks;
Expand Down
Loading