kernel, page: getpid, gettid and the 32-bit uid/gid calls answered without the kernel lock

Step 6's first part (docs/kernel-locking.md §3.1): ident_call answers them
from the ids the thread's last full syscall saw, beside clock_call and under
the same stand-down rules. getppid stays on the full path (asking for the
parent is a call into the system-wide kernel, and a reparent would go
unseen), as do the 16-bit i386 forms.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_ea9f71488b6e448da93d6c2dabf4879c
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-12 05:56:10 -04:00
1 parent 5892193eeb
commit fd2ac62850
2 files changed
+111 -1

No files matched your search

+92
View File
@@ -950,6 +950,69 @@ pub fn clock_call(
set_result(m, ctx_addr, 0).is_ok()
}
/// What a process and thread are called, as a host last saw them under its
/// lock: the answers [`ident_call`] gives without it.
#[derive(Clone, Copy, Debug, Default)]
pub struct Idents {
pub pid: u32,
pub ppid: u32,
pub tid: u32,
pub uid: u32,
pub gid: u32,
}
/// `getpid`, `getppid`, `gettid` and the 32-bit `get[e]uid`/`get[e]gid`
/// answered from `ids` with no lock (docs/kernel-locking.md §3.1), as
/// [`clock_call`] answers the clocks: a host that keeps `ids` current calls
/// this before taking its lock, and the full syscall runs whenever it returns
/// false. `ids.tid == 0` means the host has none yet, and `ids.ppid == 0`
/// leaves `getppid` to the full call. The 16-bit i386 forms
/// are left to the full call, which owns their overflow rule.
pub fn ident_call(m: &mut GuestMem<'_>, ctx_addr: u64, compat: bool, ids: &Idents) -> bool {
if gex_abi::PPC || ids.tid == 0 {
return false;
}
let raw = if compat {
m.read_u64(ctx_addr + ctx::x(0) as u64).unwrap_or(!0) & 0xffff_ffff
} else {
m.read_u64(ctx_addr + ctx::SYSCALL_NR as u64).unwrap_or(!0)
};
let v = if compat {
use crate::i386::raw as x;
match raw {
x::GETPID => ids.pid,
x::GETPPID if ids.ppid != 0 => ids.ppid,
x::GETTID => ids.tid,
x::GETUID | x::GETEUID => ids.uid,
x::GETGID | x::GETEGID => ids.gid,
_ => return false,
}
} else if gex_abi::X86 {
use crate::x86::raw as x;
match raw {
x::GETPID => ids.pid,
x::GETPPID if ids.ppid != 0 => ids.ppid,
x::GETTID => ids.tid,
x::GETUID | x::GETEUID => ids.uid,
x::GETGID | x::GETEGID => ids.gid,
_ => return false,
}
} else {
match raw {
nr::GETPID => ids.pid,
nr::GETPPID if ids.ppid != 0 => ids.ppid,
nr::GETTID => ids.tid,
nr::GETUID | nr::GETEUID => ids.uid,
nr::GETGID | nr::GETEGID => ids.gid,
_ => return false,
}
};
if gex_abi::X86 {
let _ = m.write_u64(ctx_addr + ctx::LAST_SYSCALL as u64, raw);
}
set_result(m, ctx_addr, v as u64).is_ok()
}
pub fn system_clock() -> (u64, u32) {
#[cfg(not(target_family = "wasm"))]
{
@@ -7902,6 +7965,35 @@ mod tests {
assert_eq!(k.unix_of(pfd).err(), Some(Errno::ENOTSOCK));
}
/// `ident_call` answers the id calls as the kernel does, and declines
/// what it was not given.
#[test]
fn ident_call_answers_as_the_kernel_does() {
if gex_abi::PPC || gex_abi::X86 {
return;
}
let (mut mem, mut k) = setup();
let at = k.ctx_addr;
let ids = Idents { pid: k.pid as u32, ppid: 0, tid: k.tid as u32, uid: k.uid, gid: k.gid };
for n in [nr::GETPID, nr::GETTID, nr::GETUID, nr::GETEUID, nr::GETGID, nr::GETEGID] {
let mut m = GuestMem::new(&mut mem);
set_args(&mut m, at, n, &[]);
k.syscall(&mut m, 0);
let want = ret(&m, at);
m.write_u64(at + ctx::x(0) as u64, 0x5555).unwrap();
set_args(&mut m, at, n, &[]);
assert!(ident_call(&mut m, at, false, &ids), "call {n}");
assert_eq!(ret(&m, at), want, "call {n}");
}
let mut m = GuestMem::new(&mut mem);
set_args(&mut m, at, nr::GETPPID, &[]);
assert!(!ident_call(&mut m, at, false, &ids), "no parent given");
set_args(&mut m, at, nr::GETPID, &[]);
assert!(!ident_call(&mut m, at, false, &Idents { tid: 0, ..ids }), "no thread yet");
set_args(&mut m, at, nr::CLOCK_GETTIME, &[0, 0x3000]);
assert!(!ident_call(&mut m, at, false, &ids), "not an id call");
}
/// `clock_call`, the lock-free answer, writes what the full call writes,
/// in both numberings and layouts, and declines what it cannot answer.
#[test]
+19 -1
View File
@@ -6666,6 +6666,18 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
// where process 1 never exits to call `gex_pin_report`).
check_host_magic_in(h, "a syscall");
MY_PID.store(h.kernel.pid, std::sync::atomic::Ordering::Relaxed);
// What `ident_call` answers from until this thread's next full call.
// No parent: asking for it is a call into the system-wide kernel, and
// a reparent would go unseen; `getppid` takes the full path.
IDS.with(|c| {
c.set(gex_kernel::syscalls::Idents {
pid: h.kernel.pid as u32,
ppid: 0,
tid: h.threads[cur()].kstate.tid as u32,
uid: h.kernel.uid,
gid: h.kernel.gid,
})
});
report_new_pins_in(h);
let mut gm = GuestMem::new(guest_bytes());
let kstate = h.threads[cur()].kstate.clone();
@@ -7562,6 +7574,10 @@ thread_local! {
static FAST_SIG: Cell<i32> = const { Cell::new(i32::MIN) };
/// Fast calls since this thread's last full syscall.
static FAST_RUN: Cell<u32> = const { Cell::new(0) };
/// This thread's ids as its last full syscall saw them (`ident_call`).
static IDS: Cell<gex_kernel::syscalls::Idents> = const {
Cell::new(gex_kernel::syscalls::Idents { pid: 0, ppid: 0, tid: 0, uid: 0, gid: 0 })
};
/// The canonical number of the syscall this thread just made, for
/// `wakes_nobody`; `u64::MAX` when the kernel did not run it.
static CANON_NR: Cell<u64> = const { Cell::new(u64::MAX) };
@@ -7598,7 +7614,9 @@ fn clock_fast_path(ctx: i32) -> bool {
}
let mut gm = GuestMem::new(guest_bytes());
let started = FAST_STARTED.load(Relaxed) as u128;
if !gex_kernel::syscalls::clock_call(&mut gm, ctx as u32 as u64, abi == 1, browser_clock, started) {
if !gex_kernel::syscalls::clock_call(&mut gm, ctx as u32 as u64, abi == 1, browser_clock, started)
&& !IDS.with(|c| gex_kernel::syscalls::ident_call(&mut gm, ctx as u32 as u64, abi == 1, &c.get()))
{
return false;
}
FAST_RUN.with(|c| c.set(c.get() + 1));