Skip to content

ntdll: Map executable PE sections anonymously when W^X denies RX on Android#25

Open
utkarshdalal wants to merge 5 commits into
proton_10.0from
claude/charming-hawking-2a4244
Open

ntdll: Map executable PE sections anonymously when W^X denies RX on Android#25
utkarshdalal wants to merge 5 commits into
proton_10.0from
claude/charming-hawking-2a4244

Conversation

@utkarshdalal
Copy link
Copy Markdown

What

On Android, executable PE sections (.text) can end up non-executable to the CPU even though Wine believes it set them RX. The first instruction fetch into such a section — observed on arm64ec for ntdll/kernelbase/kernel32 — faults with SIGSEGV / SEGV_ACCERR.

This adds remap_exec_anon() and wires it into map_image_into_view, so that when the RX mprotect is denied we privatize the section into anonymous memory and set it executable at load time.

Why

Wine maps PE sections MAP_PRIVATE file-backed (VPROT_WRITECOPY) and then relocates them, which dirties the COW pages. Android's SELinux W^X enforcement — specifically the execmod permission ("make executable a modified private file mapping") — denies the subsequent mprotect(..., PROT_EXEC) with EACCES. The section is then left readable/writable but not executable.

Anonymous memory is not subject to execmod (it only needs execmem, which is granted in this environment), so copying the already-relocated bytes into an anonymous mapping and setting RX there succeeds.

How

  • remap_exec_anon() (new, __ANDROID__-only): set the range readable, stage its contents into a temporary anonymous mapping, MAP_FIXED-replace the range with anonymous private memory, copy the contents back, apply the requested protection, and __builtin___clear_cache() if executable.
  • Per-section protection path: when set_vprot fails to set RX on an executable section, call remap_exec_anon and, on success, update the per-page vprot bookkeeping via set_page_vprot so Wine's recorded protection matches reality. Only ERRs if the rebake itself fails.
  • ImageMappedFlat branch: same treatment for flat/native-subsystem images where the whole image is set RX in one call.

Notes for reviewers

  • All changes are gated on __ANDROID__; non-Android builds are unchanged.
  • Runs proactively at load under virtual_mutex, replacing the need for an external reactive SIGSEGV-driven rebake (LD_PRELOAD shim).
  • Tradeoff: privatized sections lose file-backed cross-process page sharing. This is unavoidable under execmod and matches what the previous shim-based workaround already did.
  • Not yet covered: the shared_fd shared-writable-exec section path; .text does not normally take it, but it could get the same helper if needed.

🤖 Generated with Claude Code

Utkarsh Dalal and others added 5 commits May 23, 2026 01:08
…ndroid.

On Android, SELinux W^X enforcement (the execmod permission) refuses
PROT_EXEC on a modified private file mapping. Wine maps PE sections
MAP_PRIVATE file-backed and then relocates them, dirtying the COW pages,
so the final mprotect to RX in map_image_into_view is denied with EACCES
and the section is left non-executable. The first instruction fetch into
such a section (observed on arm64ec ntdll/kernelbase/kernel32) then faults
with SEGV_ACCERR.

Add remap_exec_anon(), which privatizes the offending range into anonymous
memory (which only needs execmem, not execmod) preserving the already
relocated contents, sets the requested protection, and flushes the
instruction cache. Wire it into both the normal per-section protection
path and the ImageMappedFlat branch, updating the per-page vprot
bookkeeping so Wine's view of the protection stays accurate. All changes
are gated on __ANDROID__.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…roid.

Android SELinux execmod denies PROT_EXEC on modified private file mappings
(COW-dirtied by PE relocations). Detecting this via set_vprot() failure is
unreliable because LD_PRELOAD shims strip PROT_EXEC and return success,
hiding the kernel EACCES. Privatize executable sections into anonymous
memory before set_vprot() so the subsequent mprotect(RX) targets anon
memory and only requires execmem, which the app sandbox permits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
remap_exec_anon's __builtin___clear_cache is gated on PROT_EXEC in the
unix_prot argument. Passing PROT_READ|PROT_WRITE skipped the flush, which
on aarch64 leaves stale icache for the freshly-copied bytes (mprotect on
Linux/aarch64 does not invalidate icache). Pass the section's real unix
prot via get_unix_prot(vprot) so the flush fires.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The W^X privatization only matters when PE bytes execute as native ARM
hardware code -- i.e. arm64ec. x86_64 Wine on Android runs PE .text under
FEX/box64, which only reads it (no PROT_EXEC at hardware level), so it
doesn't hit execmod and shouldn't pay the page-cache-sharing cost.

Compile-time gate (__ANDROID__ && __aarch64__) skips the helper entirely
on x86_64 Android builds. Runtime gate (is_arm64ec()) skips the privatize
on a hypothetical plain ARM64 Wine, since current_machine is a compile-
time constant equal to ARM64 on aarch64 builds, leaving only the
main_image_info.Machine check at runtime.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Android Wine build in this repo is always aarch64, so the compile-
time __aarch64__ gate was noise. Runtime is_arm64ec() is the standard
pattern used throughout ntdll (thread.c, loader.c, process.c, signal_arm64.c)
and is sufficient on its own.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant