#28

ntdll iOS: getsockopt, /dev/urandom and ReadFileScatter data reach buffers in RWX memory

Openashleigh-hopkinswants to merge intofrom

Companion to willfaust/Madeira#248 (https://github.com/willfaust/Madeira/pull/248), whose RWX-memory commits route kernel and wineserver writes into PAGE_EXECUTE_READWRITE memory through its RW alias. The two should land together; Madeira's wine pin can be bumped afterwards.

Problem

On iOS, Madeira maps anonymous PAGE_EXECUTE_READWRITE memory as an RX view with a separate RW alias (W^X). Stores into it from user space are redirected to the alias by Madeira's fault handler. The kernel cannot write such a buffer, so a system call that fills it fails with EFAULT. Three paths in this tree let the kernel write straight into a caller's buffer, and each misbehaves when that buffer is RWX memory, where Windows succeeds:

  • do_getsockopt (the AFD getsockopt ioctls) and the IP_DONTFRAG / IPV6_DONTFRAG queries return WSAEFAULT.
  • read_dev_urandom (used by NtQuerySystemInformation(SystemInterruptInformation), among others) reports success with the buffer left unfilled.
  • NtReadFileScatter returns STATUS_END_OF_FILE (ERROR_HANDLE_EOF) without data. The EFAULT from pread() is overwritten because nothing was read.

Cause

getsockopt(), read() and pread() are called with the caller's buffer as the destination, so the kernel does the write.

Change

  • do_getsockopt, IP_DONTFRAG, IPV6_DONTFRAG: getsockopt() fills a local copy, which is then copied into the caller's buffer.
    • The local copy is 256 bytes on the stack, or malloc'd for larger sizes.
    • The DONTFRAG queries use a DWORD-sized local and the same length cap as before.
  • read_dev_urandom: reads 256-byte chunks and copies each one. A short read is now completed instead of leaving the rest of the buffer unfilled.
  • NtReadFileScatter: reads through virtual_locked_pread/virtual_locked_read, as NtReadFile does. Madeira's versions of those fill RWX memory through the RW alias (companion PR).

Other buffers are unaffected, apart from the completed short read in read_dev_urandom.

Evidence

These results come from our downstream builds (Mac Catalyst and iPad), not from this branch. The three functions are byte-identical at 257f271c and in those builds before this change.

The test program is a small Windows x64 program. It calls getsockopt(SO_KEEPALIVE) on a loopback socket, NtQuerySystemInformation(SystemInterruptInformation) (48 bytes) and ReadFileScatter (one page, unbuffered file). The destination is heap, stack, image .data, an RWX image section, and four places in a 64 KB VirtualAlloc(PAGE_EXECUTE_READWRITE) region.

  • Before (with the Madeira half already applied), in every anonymous-RWX case:
    • getsockopt failed with WSAEFAULT (10014).
    • SystemInterruptInformation returned success with 0 of 48 bytes written.
    • ReadFileScatter failed with ERROR_HANDLE_EOF (38) and 0 bytes.
  • After, in the same cases on Mac and iPad:
    • getsockopt succeeds.
    • The interrupt information is filled. The check counts bytes changed from a fill pattern, so 47 of 48 happens by chance with random data.
    • ReadFileScatter reads the full page and the data matches.
    • The heap, stack and image cases are unchanged.

Validation

What I ran on this branch:

  • clang -fsyntax-only with Madeira's build/ntdll-unix/build.sh flags (iPhoneOS 27.0 SDK, arm64), which is how Madeira compiles these files, for file.c, socket.c and system.c. The diagnostics are identical before and after:
    • file.c: 2 pre-existing warnings.
    • socket.c: 4 pre-existing warnings.
    • system.c: none. It was compiled with an extra include path so that its ../../../../build/madeira_cfg.h resolves to a Madeira checkout.
  • Both IP_DONTFRAG and IPV6_DONTFRAG are defined in the iOS SDK, so the changed branches are compiled. I confirmed this with the preprocessor.
  • Not run: this branch on a device, a full Wine or Madeira build, and any test that exercises the copies on the host. The redirect into the alias only exists in Madeira's iOS fault handler.

Notes / risks

  • Land together with the Madeira PR.
    • NtReadFileScatter only gains anything with the Madeira half. Without it, virtual_locked_pread/read behave like plain pread/read for RWX memory, giving the same STATUS_END_OF_FILE as today.
    • The getsockopt and /dev/urandom changes need only the store emulator already in Madeira main.
    • Neither PR breaks the build without the other. Madeira's wine submodule pin needs a bump after this merges.
  • Invalid buffer pointer (not tested). Before, the kernel returned EFAULT (WSAEFAULT for getsockopt; ignored in read_dev_urandom). Now the copy faults in ntdll's unix code and goes through the signal handler's in-syscall fault path (handle_syscall_fault, which returns the exception status from the syscall).
  • Not offered to WineHQ. The motivation is Madeira's W^X mapping. The NtReadFileScatter change alone would also make it handle write-watched buffers as NtReadFile does, which could be offered upstream separately.
  • Out of scope. getsockopt(SO_KEEPALIVE) returns the BSD flag value (8) with length 1 rather than TRUE, on every buffer kind. This is a separate Wine-on-macOS difference.

Licence, DCO, AI assistance

  • Licence. Contributed under LGPL-2.1-or-later, the licence of this branch and of upstream Wine, per CONTRIBUTING.md. Upstream notices are unchanged. Recording the contribution in LICENSE-MADEIRA.md is left to the maintainer, as with earlier pull requests.
  • DCO. The submitter certifies the Developer Certificate of Origin and signs off the commit before the PR is opened (for example git commit --amend --signoff). The commit was prepared without a Signed-off-by line, so that the sign-off is the submitter's own.
  • AI assistance. This PR was prepared with AI assistance. An AI coding assistant ported the downstream commit and drafted this description.

Activity

2
125hz7d ago

Conflicts with madeira-lgpl now; could you rebase?