From eecede59ea5a0db197e9283f642649a69e4d6d43 Mon Sep 17 00:00:00 2001 From: Philip Dye Date: Thu, 7 May 2026 02:26:47 -0400 Subject: [PATCH 1/2] cygwin: fix off-by-one in utsname.sysname length Cygwin's defines _UTSNAME_LENGTH as 65, and all six fields of struct utsname are 65 bytes: struct utsname { char sysname [65]; char nodename [65]; char release [65]; char version [65]; char machine [65]; char domainname[65]; }; Verified by C-side struct probe on a Cygwin x86_64 host: sizeof(struct utsname) = 390 (= 6 * 65) offsetof(sysname) = 0 offsetof(nodename) = 65 offsetof(release) = 130 offsetof(version) = 195 offsetof(machine) = 260 The libc crate declares sysname as 66 bytes, which shifts every other field one byte forward in Rust's view of the struct. Reading the fields with CStr::from_ptr therefore drops the first character of nodename, release, version, and machine. sysname itself is unaffected because field 0 starts at offset 0 either way. Empirically, on a host where `uname -a` reports CYGWIN_NT-10.0-26200 xps-ne 3.6.7-1.x86_64 ... a Rust caller of libc::uname() sees: sysname = "CYGWIN_NT-10.0-26200" (correct) nodename = "ps-ne" (lost 'x') release = ".6.7-1.x86_64" (lost '3') machine = "86_64" (lost 'x') Downstream consequences include silently mangled hostnames in Rust-based logging/telemetry on Cygwin, broken `uname -m` output in `uutils/coreutils`, and malformed wheel platform tags produced by `maturin` (cygwin_86_64 instead of cygwin_x86_64) which Cygwin Python's `pip` then refuses with "is not a supported wheel on this platform." Fix: declare sysname at the correct length of 65 bytes, matching the other five fields. After the fix, all five public fields sit at the correct C offsets and CStr reads return the full strings. --- src/unix/cygwin/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unix/cygwin/mod.rs b/src/unix/cygwin/mod.rs index 56ee0a5d105e..3f70ef5247be 100644 --- a/src/unix/cygwin/mod.rs +++ b/src/unix/cygwin/mod.rs @@ -447,7 +447,7 @@ s! { } pub struct utsname { - pub sysname: [c_char; 66], + pub sysname: [c_char; 65], pub nodename: [c_char; 65], pub release: [c_char; 65], pub version: [c_char; 65], From 7facc232b74473b1fc3ed46692c8d51e5978d7c8 Mon Sep 17 00:00:00 2001 From: Philip Dye Date: Thu, 7 May 2026 03:05:14 -0400 Subject: [PATCH 2/2] cygwin: regression test for utsname.sysname length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a runtime test that calls libc::uname() and asserts: 1. nodename matches gethostname() — guards against the field shifting back to a misaligned offset. 2. every variable-length field reads as a non-empty string. The C-side struct layout cross-validation in libc-test/ctest already catches a regression of the [c_char; 65] length at compile time. This test is a runtime safety net in case utsname is ever moved to ctest's skip list for Cygwin. --- libc-test/tests/cygwin_utsname.rs | 72 +++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 libc-test/tests/cygwin_utsname.rs diff --git a/libc-test/tests/cygwin_utsname.rs b/libc-test/tests/cygwin_utsname.rs new file mode 100644 index 000000000000..71370c72268b --- /dev/null +++ b/libc-test/tests/cygwin_utsname.rs @@ -0,0 +1,72 @@ +//! Verify that `libc::uname()` populates the `utsname` struct with each field +//! starting at the correct C offset on Cygwin. +//! +//! Cygwin's `` declares all six fields as 65 bytes +//! (`_UTSNAME_LENGTH`). If any field is mis-sized in the Rust declaration, +//! every subsequent field shifts in the struct and `CStr::from_ptr` reads +//! that field starting at the wrong byte. The most observable consequence +//! is that `nodename` no longer matches `gethostname()`, so this test is +//! the smallest behavioural check that catches such a regression. + +#![cfg(target_os = "cygwin")] + +use std::ffi::CStr; +use std::mem::MaybeUninit; + +#[test] +fn uname_nodename_matches_gethostname() { + let mut uts = MaybeUninit::::uninit(); + let mut host = [0u8; 256]; + + unsafe { + assert_ne!( + libc::uname(uts.as_mut_ptr()), + -1, + "uname() failed", + ); + assert_ne!( + libc::gethostname(host.as_mut_ptr() as *mut libc::c_char, host.len()), + -1, + "gethostname() failed", + ); + + let uts = uts.assume_init(); + let nodename = CStr::from_ptr(uts.nodename.as_ptr()) + .to_str() + .expect("uname.nodename is not valid UTF-8"); + let hostname = CStr::from_ptr(host.as_ptr() as *const libc::c_char) + .to_str() + .expect("gethostname() returned non-UTF-8"); + + assert_eq!( + nodename, hostname, + "uname.nodename and gethostname() must agree", + ); + } +} + +#[test] +fn uname_fields_are_non_empty() { + let mut uts = MaybeUninit::::uninit(); + + unsafe { + assert_ne!(libc::uname(uts.as_mut_ptr()), -1, "uname() failed"); + let uts = uts.assume_init(); + + let read = |buf: &[libc::c_char], name: &str| -> String { + CStr::from_ptr(buf.as_ptr()) + .to_str() + .unwrap_or_else(|_| panic!("uname.{name} is not valid UTF-8")) + .to_owned() + }; + + // Every variable-length field must read as a non-empty string. + // A leading-zero byte here would mean the field is being read + // from a misaligned offset (the bug this test guards against). + assert!(!read(&uts.sysname, "sysname").is_empty()); + assert!(!read(&uts.nodename, "nodename").is_empty()); + assert!(!read(&uts.release, "release").is_empty()); + assert!(!read(&uts.version, "version").is_empty()); + assert!(!read(&uts.machine, "machine").is_empty()); + } +}