Skip to content

Commit 0fe9f3f

Browse files
fix(abi): ustat/vtimes/sprofil are live glibc calls, not ENOSYS stubs (bd-86hcwh)
Resolves the last 3 of the 5 divergences bd-r71n1b's relit target exposed, plus 2 more that only became visible once the first of them stopped failing early. Each was MEASURED against live glibc 2.42 before deciding which side was wrong, and they did not all go the same way: ustat fl WRONG. fl returned a blanket ENOSYS on the premise that Linux 4.18 removed the syscall. It did not; only glibc's header exposure went. Kernel probe: ustat(0,buf) -1/EINVAL, ustat(dev(/),buf) 0, ustat(dev(/),NULL) -1/EFAULT. Now issues SYS_USTAT (x86_64; the aarch64 generic ABI really has no ustat, so ENOSYS stays there). vtimes fl WRONG. glibc keeps it as a live getrusage wrapper returning 0. Layout derived by measurement: 10 words, of which vm_ixrss and vm_maxrss are declared but never assigned. The unit is 1/60 s, not the 1/HZ its own header claims - 84 successive carry points of vm_utime were bracketed against getrusage and every one landed on k*1000000/60 us, ruling out the 16667- and 16666-per-unit variants. sprofil fl WRONG. glibc succeeds and writes the sampling period into tvp ({0, 1000000/__profile_frequency()}) whatever profcnt says. fl now matches that contract; it still does not deliver samples, and neither does profil next door - that gap is filed as bd-br5a1b rather than papered over. inet6_opt_init THE TEST was wrong. It asserted the call ZEROES the first two header bytes. Measured: ip6h_nxt is left to the caller and ip6h_len is set from the BUFFER length (extlen=16 -> 1, 2040 -> 254), while 0/15/-8/2056 are rejected without writing and a NULL extbuf reports 2 for any extlen. fl already agreed with the host on all of it. profil THE TEST was wrong. It seeded the HOST's errno with EAGAIN and then CLEARED fl's, so it compared a survived sentinel against a zero it had written itself. Both sides leave errno alone; seed both. Tests are differentials against the live host, not constants: ustat compares kernel-zeroed struct bytes and asserts EINVAL on an unmounted device (which a stub cannot fake without issuing the syscall); vtimes sandwiches each fl call between two host calls and requires every monotone counter to land inside that bracket, which is what pins the time unit; sprofil asserts the period written to tvp, which a bare `return 0` fails. glibc_internal_abi_test: 264 passed / 5 failed at relight, now 272 / 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 063fc04 commit 0fe9f3f

3 files changed

Lines changed: 469 additions & 53 deletions

File tree

crates/frankenlibc-abi/src/glibc_internal_abi.rs

Lines changed: 120 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5224,19 +5224,34 @@ pub unsafe extern "C" fn profil(
52245224
let _ = (buf, bufsiz, offset, scale);
52255225
0 // success no-op
52265226
}
5227-
// sprofil: not available on Linux — return ENOSYS
5227+
/// `sprofil` — multi-region PC-sample profiling.
5228+
///
5229+
/// glibc does NOT report ENOSYS here, which is what fl returned while this arm
5230+
/// was dark. Measured against glibc 2.42: `sprofil(NULL, 0, NULL, 0)` succeeds
5231+
/// with errno untouched, and a non-NULL `tvp` receives the sampling period
5232+
/// `{0, 1000000 / __profile_frequency()}` — `{0, 10000}` on this host —
5233+
/// independently of `profcnt`.
5234+
///
5235+
/// Like `profil` next door, fl accepts and reports the period but does not
5236+
/// itself deliver samples; bd-br5a1b tracks the real sampling engine. glibc
5237+
/// dereferences `profp` unconditionally once `profcnt` is non-zero and
5238+
/// segfaults on a NULL one — fl returns the safe default instead.
52285239
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
52295240
pub unsafe extern "C" fn sprofil(
52305241
profp: *mut c_void,
52315242
profcnt: c_int,
52325243
tvp: *mut c_void,
52335244
flags: c_uint,
52345245
) -> c_int {
5235-
let _ = (profp, profcnt, tvp, flags);
5236-
unsafe {
5237-
crate::errno_abi::set_abi_errno(libc::ENOSYS);
5246+
let _ = (profp, profcnt, flags);
5247+
if !tvp.is_null() && !tracked_output_too_short(tvp, size_of::<libc::timeval>()) {
5248+
let period = libc::timeval {
5249+
tv_sec: 0,
5250+
tv_usec: (1_000_000 / unsafe { __profile_frequency() }) as _,
5251+
};
5252+
unsafe { tvp.cast::<libc::timeval>().write(period) };
52385253
}
5239-
-1
5254+
0
52405255
}
52415256

52425257
// Misc POSIX functions
@@ -7691,13 +7706,38 @@ pub unsafe extern "C" fn uselib(_library: *const c_char) -> c_int {
76917706
unsafe { set_abi_errno(libc::ENOSYS) };
76927707
-1
76937708
}
7694-
// ustat: removed in Linux 4.18 — return ENOSYS
7709+
/// `ustat` — legacy per-device filesystem statistics.
7710+
///
7711+
/// glibc dropped this from its headers in 2.28 but still ships the compat
7712+
/// symbol, and it is still a plain wrapper over the live `ustat` syscall
7713+
/// (`fs/statfs.c`). Measured on glibc 2.42 / Linux 6.17: `ustat(0, buf)` is
7714+
/// `-1/EINVAL` because device 0 has no mounted superblock — NOT `ENOSYS`.
7715+
/// The syscall is absent from the aarch64 generic ABI, where `ENOSYS` is right.
7716+
///
7717+
/// The kernel writes a 32-byte `struct ustat`, so a tracked destination that
7718+
/// cannot hold one is refused with `EFAULT` rather than letting the kernel
7719+
/// write past it.
7720+
#[cfg(target_arch = "x86_64")]
76957721
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
7696-
pub unsafe extern "C" fn ustat(dev: c_uint, ubuf: *mut c_void) -> c_int {
7697-
let _ = (dev, ubuf);
7698-
unsafe {
7699-
crate::errno_abi::set_abi_errno(libc::ENOSYS);
7722+
pub unsafe extern "C" fn ustat(dev: libc::dev_t, ubuf: *mut c_void) -> c_int {
7723+
const USTAT_SIZE: usize = 32;
7724+
if !ubuf.is_null() && tracked_output_too_short(ubuf, USTAT_SIZE) {
7725+
unsafe { crate::errno_abi::set_abi_errno(libc::EFAULT) };
7726+
return -1;
77007727
}
7728+
match unsafe { raw_syscall::sys_ustat(dev as usize, ubuf.cast::<u8>()) } {
7729+
Ok(()) => 0,
7730+
Err(e) => {
7731+
unsafe { crate::errno_abi::set_abi_errno(e) };
7732+
-1
7733+
}
7734+
}
7735+
}
7736+
7737+
#[cfg(not(target_arch = "x86_64"))]
7738+
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
7739+
pub unsafe extern "C" fn ustat(_dev: libc::dev_t, _ubuf: *mut c_void) -> c_int {
7740+
unsafe { crate::errno_abi::set_abi_errno(libc::ENOSYS) };
77017741
-1
77027742
}
77037743
// utime: forward to libc
@@ -7756,14 +7796,79 @@ pub unsafe extern "C" fn vlimit(resource: c_int, value: c_int) -> c_int {
77567796
limit.rlim_cur = value as libc::rlim_t;
77577797
unsafe { crate::resource_abi::setrlimit(resource, &limit) }
77587798
}
7759-
// vtimes: obsolete BSD process times — return ENOSYS
7799+
/// `struct vtimes` is ten 32-bit fields (40 bytes). glibc fills only eight of
7800+
/// them from `getrusage`; word 3 (`vm_ixrss`) and word 4 (`vm_maxrss`) are
7801+
/// declared but never assigned, so whatever the caller left there survives.
7802+
const VTIMES_WORDS: usize = 10;
7803+
const VTIMES_SIZE: usize = VTIMES_WORDS * 4;
7804+
7805+
/// Convert a `timeval` to vtimes' unit.
7806+
///
7807+
/// The header calls these "units of 1/HZ seconds", which is wrong: measured
7808+
/// against live glibc 2.42 on a CLK_TCK=100 host, the unit is 1/60 s. Bracketing
7809+
/// 84 successive carry points of `vm_utime` against `getrusage` located every
7810+
/// one of them at `k * 1000000/60` microseconds to within a few microseconds —
7811+
/// e.g. k=31 fell in [516664, 516666] where 1e6*31/60 = 516666.7, while the
7812+
/// 16667-per-unit and 16666-per-unit variants predict 516677 and 516646.
7813+
#[inline]
7814+
fn timeval_to_vtimes(sec: i64, usec: i64) -> i32 {
7815+
(sec * 60 + (usec * 60) / 1_000_000) as i32
7816+
}
7817+
7818+
/// `vtimes` — obsolete BSD process resource usage.
7819+
///
7820+
/// NOT an ENOSYS stub, which is what fl returned while this arm was dark: glibc
7821+
/// keeps it as a live wrapper that samples `getrusage` into the caller's
7822+
/// `struct vtimes`. Measured on glibc 2.42: `vtimes(NULL, NULL)` returns 0 with
7823+
/// errno untouched, and a filled struct leaves words 3 and 4 exactly as the
7824+
/// caller had them.
77607825
#[cfg_attr(not(debug_assertions), unsafe(no_mangle))]
77617826
pub unsafe extern "C" fn vtimes(current: *mut c_void, child: *mut c_void) -> c_int {
7762-
let _ = (current, child);
7763-
unsafe {
7764-
crate::errno_abi::set_abi_errno(libc::ENOSYS);
7827+
unsafe fn fill(dest: *mut c_void, who: c_int) -> Result<(), i32> {
7828+
if tracked_output_too_short(dest, VTIMES_SIZE) {
7829+
return Err(libc::EFAULT);
7830+
}
7831+
let mut usage = unsafe { std::mem::zeroed::<libc::rusage>() };
7832+
unsafe {
7833+
raw_syscall::sys_getrusage(who, (&raw mut usage).cast::<u8>())?;
7834+
}
7835+
let words = dest.cast::<i32>();
7836+
unsafe {
7837+
words.write(timeval_to_vtimes(
7838+
usage.ru_utime.tv_sec as i64,
7839+
usage.ru_utime.tv_usec as i64,
7840+
));
7841+
words.add(1).write(timeval_to_vtimes(
7842+
usage.ru_stime.tv_sec as i64,
7843+
usage.ru_stime.tv_usec as i64,
7844+
));
7845+
words.add(2).write(usage.ru_idrss as i32);
7846+
// Words 3 (vm_ixrss) and 4 (vm_maxrss) are deliberately skipped —
7847+
// glibc never assigns them, and a test that fills the buffer with a
7848+
// sentinel can see the difference.
7849+
words.add(5).write(usage.ru_majflt as i32);
7850+
words.add(6).write(usage.ru_minflt as i32);
7851+
words.add(7).write(usage.ru_nswap as i32);
7852+
words.add(8).write(usage.ru_inblock as i32);
7853+
words.add(9).write(usage.ru_oublock as i32);
7854+
}
7855+
Ok(())
7856+
}
7857+
7858+
let mut result = Ok(());
7859+
if !current.is_null() {
7860+
result = unsafe { fill(current, libc::RUSAGE_SELF) };
7861+
}
7862+
if result.is_ok() && !child.is_null() {
7863+
result = unsafe { fill(child, libc::RUSAGE_CHILDREN) };
7864+
}
7865+
match result {
7866+
Ok(()) => 0,
7867+
Err(e) => {
7868+
unsafe { crate::errno_abi::set_abi_errno(e) };
7869+
-1
7870+
}
77657871
}
7766-
-1
77677872
}
77687873

77697874
// Legacy BSD/V7 regex (re_comp/re_exec) — shared compiled-pattern state. These

0 commit comments

Comments
 (0)