Skip to content

cygwin: fix off-by-one in utsname.sysname length#5099

Open
phdye wants to merge 2 commits into
rust-lang:mainfrom
phdye-cygwin-rust:cygwin-utsname-sysname-65
Open

cygwin: fix off-by-one in utsname.sysname length#5099
phdye wants to merge 2 commits into
rust-lang:mainfrom
phdye-cygwin-rust:cygwin-utsname-sysname-65

Conversation

@phdye
Copy link
Copy Markdown
Contributor

@phdye phdye commented May 7, 2026

Description

libc declares the Cygwin utsname struct's sysname field as [c_char; 66], but Cygwin's <sys/utsname.h> declares all six fields as _UTSNAME_LENGTH = 65 bytes. The over-declaration shifts every subsequent field one byte forward in Rust's view of the struct, so CStr::from_ptr reads nodename, release, version, machine, and domainname starting at byte 1 instead of byte 0 — silently dropping the first character of each.

This PR changes a single character (6665) and adds a runtime regression test.

Empirical effect on a Cygwin x86_64 host where uname -a reports CYGWIN_NT-10.0-26200 xps-ne 3.6.7-1.x86_64 ...:

Field Before fix Expected
sysname CYGWIN_NT-10.0-26200 CYGWIN_NT-10.0-26200
nodename ps-ne xps-ne
release .6.7-1.x86_64 3.6.7-1.x86_64
machine 86_64 x86_64

sysname reads correctly because field 0 starts at offset 0 either way; subsequent fields each begin one byte too late.

Downstream this propagates to platform-info, uutils/coreutils's uname -m, and maturin's wheel-tag formatter — which produces *-cygwin_86_64.whl (instead of cygwin_x86_64) that Cygwin Python's pip then refuses with is not a supported wheel on this platform.

Sources

Cygwin's <sys/utsname.h> in the newlib-cygwin tree:

https://sourceware.org/git/?p=newlib-cygwin.git;a=blob;f=winsup/cygwin/include/sys/utsname.h

#define _UTSNAME_LENGTH 65

struct utsname
{
  char sysname[_UTSNAME_LENGTH];
  char nodename[_UTSNAME_LENGTH];
  char release[_UTSNAME_LENGTH];
  char version[_UTSNAME_LENGTH];
  char machine[_UTSNAME_LENGTH];
#if __GNU_VISIBLE
  char domainname[_UTSNAME_LENGTH];
#else
  char __domainname[_UTSNAME_LENGTH];
#endif
};

Checklist

  • Relevant tests in libc-test/semver have been updated — N/A; correcting layout of an existing struct field, not adding/removing symbols.
  • No placeholder or unstable values like *LAST or *MAX are included
  • Tested locally — cargo test --target x86_64-pc-cygwin passes on libc-0.2 with this fix; passes on main with this fix plus the cpuset_t typo fix from companion PR cygwin: fix cpuset_t typo in CPU_ZERO #5098. The added regression test verifies uname.nodename == gethostname() and that all variable-length fields read non-empty.

Note: this PR is independent but main does not currently build on x86_64-pc-cygwin due to a separate cpuset_t typo, addressed in companion PR #5098. The fix here was verified end-to-end on both the libc-0.2 branch (which builds cleanly) and on main with both fixes stacked.

@rustbot label +stable-nominated

phdye added 2 commits May 7, 2026 02:26
Cygwin's <sys/utsname.h> 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.
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.
@rustbot rustbot added S-waiting-on-review stable-nominated This PR should be considered for cherry-pick to libc's stable release branch labels May 7, 2026
@phdye phdye mentioned this pull request May 7, 2026
3 tasks
Copy link
Copy Markdown
Member

@JohnTitor JohnTitor May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you drop this test? We don't have concrete testing structure for this type and I'd figure out it before actually adding anything.

View changes since the review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-author stable-nominated This PR should be considered for cherry-pick to libc's stable release branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants