From 013fd13a93d52f8b551cc139d350050b0e77f232 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Sat, 23 May 2026 01:08:51 +0530 Subject: [PATCH 1/5] ntdll: Map executable PE sections anonymously when W^X denies RX on Android. 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 --- dlls/ntdll/unix/virtual.c | 55 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/dlls/ntdll/unix/virtual.c b/dlls/ntdll/unix/virtual.c index 963ebd279e2b..c7ce5aaef2bd 100644 --- a/dlls/ntdll/unix/virtual.c +++ b/dlls/ntdll/unix/virtual.c @@ -3219,6 +3219,41 @@ static IMAGE_BASE_RELOCATION *process_relocation_block( char *page, IMAGE_BASE_R } +#ifdef __ANDROID__ +/*********************************************************************** + * remap_exec_anon + * + * Android W^X (SELinux execmod) refuses PROT_EXEC on a *modified* private + * file mapping. PE sections are mapped MAP_PRIVATE file-backed and then + * relocated (which dirties the COW pages), so the final mprotect to RX is + * denied with EACCES. Anonymous memory only needs execmem, which is granted, + * so privatize the range into anonymous memory preserving the (already + * relocated) contents and set the requested protection. virtual_mutex must + * be held by caller. + */ +static BOOL remap_exec_anon( void *addr, size_t size, int unix_prot ) +{ + void *stage; + + if (mprotect( addr, size, PROT_READ )) return FALSE; /* ensure readable */ + stage = mmap( NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0 ); + if (stage == MAP_FAILED) return FALSE; + memcpy( stage, addr, size ); + if (mmap( addr, size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0 ) == MAP_FAILED) + { + munmap( stage, size ); + return FALSE; + } + memcpy( addr, stage, size ); + munmap( stage, size ); + if (mprotect( addr, size, unix_prot )) return FALSE; + if (unix_prot & PROT_EXEC) __builtin___clear_cache( (char *)addr, (char *)addr + size ); + return TRUE; +} +#endif + + /*********************************************************************** * map_image_into_view * @@ -3286,7 +3321,14 @@ static NTSTATUS map_image_into_view( struct file_view *view, const WCHAR *filena } /* set the image protections */ - set_vprot( view, ptr, total_size, VPROT_COMMITTED | VPROT_READ | VPROT_WRITECOPY | VPROT_EXEC ); + if (!set_vprot( view, ptr, total_size, VPROT_COMMITTED | VPROT_READ | VPROT_WRITECOPY | VPROT_EXEC )) + { +#ifdef __ANDROID__ + BYTE flat_vprot = VPROT_COMMITTED | VPROT_READ | VPROT_WRITECOPY | VPROT_EXEC; + if (remap_exec_anon( ptr, total_size, get_unix_prot( flat_vprot ) )) + set_page_vprot( ptr, total_size, flat_vprot ); +#endif + } /* no relocations are performed on non page-aligned binaries */ return STATUS_SUCCESS; @@ -3442,8 +3484,19 @@ static NTSTATUS map_image_into_view( struct file_view *view, const WCHAR *filena if (sec->Characteristics & IMAGE_SCN_MEM_EXECUTE) vprot |= VPROT_EXEC; if (!set_vprot( view, ptr + sec->VirtualAddress, size, vprot ) && (vprot & VPROT_EXEC)) + { +#ifdef __ANDROID__ + /* W^X (execmod) denied RX on the relocated file-backed section; + * privatize it into anonymous memory and retry. */ + if (remap_exec_anon( ptr + sec->VirtualAddress, size, get_unix_prot( vprot ) )) + { + set_page_vprot( ptr + sec->VirtualAddress, size, vprot ); + continue; + } +#endif ERR( "failed to set %08x protection on %s section %.8s, noexec filesystem?\n", (int)sec->Characteristics, debugstr_w(filename), sec->Name ); + } } #ifdef VALGRIND_LOAD_PDB_DEBUGINFO From a866fe5b20730e5084621c3617605026097eedf5 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Sat, 23 May 2026 14:52:41 +0530 Subject: [PATCH 2/5] ntdll: Proactively remap executable PE sections to anon memory on Android. 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 --- dlls/ntdll/unix/virtual.c | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/dlls/ntdll/unix/virtual.c b/dlls/ntdll/unix/virtual.c index c7ce5aaef2bd..a1bf1f88e420 100644 --- a/dlls/ntdll/unix/virtual.c +++ b/dlls/ntdll/unix/virtual.c @@ -3320,15 +3320,14 @@ static NTSTATUS map_image_into_view( struct file_view *view, const WCHAR *filena return status; /* Windows refuses to load in that case too */ } - /* set the image protections */ - if (!set_vprot( view, ptr, total_size, VPROT_COMMITTED | VPROT_READ | VPROT_WRITECOPY | VPROT_EXEC )) - { #ifdef __ANDROID__ - BYTE flat_vprot = VPROT_COMMITTED | VPROT_READ | VPROT_WRITECOPY | VPROT_EXEC; - if (remap_exec_anon( ptr, total_size, get_unix_prot( flat_vprot ) )) - set_page_vprot( ptr, total_size, flat_vprot ); + /* Proactive privatization for the whole flat image (see comment on + * the per-section path) -- detecting EACCES via set_vprot failure is + * unreliable when an LD_PRELOAD shim strips PROT_EXEC. */ + remap_exec_anon( ptr, total_size, PROT_READ | PROT_WRITE ); #endif - } + /* set the image protections */ + set_vprot( view, ptr, total_size, VPROT_COMMITTED | VPROT_READ | VPROT_WRITECOPY | VPROT_EXEC ); /* no relocations are performed on non page-aligned binaries */ return STATUS_SUCCESS; @@ -3483,20 +3482,24 @@ static NTSTATUS map_image_into_view( struct file_view *view, const WCHAR *filena if (sec->Characteristics & IMAGE_SCN_MEM_WRITE) vprot |= VPROT_WRITECOPY; if (sec->Characteristics & IMAGE_SCN_MEM_EXECUTE) vprot |= VPROT_EXEC; - if (!set_vprot( view, ptr + sec->VirtualAddress, size, vprot ) && (vprot & VPROT_EXEC)) - { #ifdef __ANDROID__ - /* W^X (execmod) denied RX on the relocated file-backed section; - * privatize it into anonymous memory and retry. */ - if (remap_exec_anon( ptr + sec->VirtualAddress, size, get_unix_prot( vprot ) )) - { - set_page_vprot( ptr + sec->VirtualAddress, size, vprot ); - continue; - } + /* Android W^X (SELinux execmod) refuses PROT_EXEC on a modified + * private file mapping, which is exactly what a relocated PE .text + * is. Detecting that via set_vprot() failure is unreliable -- LD_PRELOAD + * shims commonly strip PROT_EXEC and report success to keep Wine + * load paths working, so the kernel's EACCES never reaches us. + * Privatize executable sections into anonymous memory PROACTIVELY, + * before the final mprotect: anon memory only needs execmem, so the + * subsequent set_vprot() succeeds on its own merits without needing + * to detect a failure that may have been hidden. */ + if (vprot & VPROT_EXEC) + remap_exec_anon( ptr + sec->VirtualAddress, size, + PROT_READ | PROT_WRITE ); #endif + + if (!set_vprot( view, ptr + sec->VirtualAddress, size, vprot ) && (vprot & VPROT_EXEC)) ERR( "failed to set %08x protection on %s section %.8s, noexec filesystem?\n", (int)sec->Characteristics, debugstr_w(filename), sec->Name ); - } } #ifdef VALGRIND_LOAD_PDB_DEBUGINFO From 3424774eeda5278c21f15114041396a58c6ffbb1 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Sat, 23 May 2026 14:54:31 +0530 Subject: [PATCH 3/5] ntdll: Use real exec prot in remap_exec_anon so icache is flushed. 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 --- dlls/ntdll/unix/virtual.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dlls/ntdll/unix/virtual.c b/dlls/ntdll/unix/virtual.c index a1bf1f88e420..78155c40e35c 100644 --- a/dlls/ntdll/unix/virtual.c +++ b/dlls/ntdll/unix/virtual.c @@ -3324,7 +3324,8 @@ static NTSTATUS map_image_into_view( struct file_view *view, const WCHAR *filena /* Proactive privatization for the whole flat image (see comment on * the per-section path) -- detecting EACCES via set_vprot failure is * unreliable when an LD_PRELOAD shim strips PROT_EXEC. */ - remap_exec_anon( ptr, total_size, PROT_READ | PROT_WRITE ); + remap_exec_anon( ptr, total_size, + get_unix_prot( VPROT_COMMITTED | VPROT_READ | VPROT_WRITECOPY | VPROT_EXEC ) ); #endif /* set the image protections */ set_vprot( view, ptr, total_size, VPROT_COMMITTED | VPROT_READ | VPROT_WRITECOPY | VPROT_EXEC ); @@ -3494,7 +3495,7 @@ static NTSTATUS map_image_into_view( struct file_view *view, const WCHAR *filena * to detect a failure that may have been hidden. */ if (vprot & VPROT_EXEC) remap_exec_anon( ptr + sec->VirtualAddress, size, - PROT_READ | PROT_WRITE ); + get_unix_prot( vprot ) ); #endif if (!set_vprot( view, ptr + sec->VirtualAddress, size, vprot ) && (vprot & VPROT_EXEC)) From 2525b3056eb8967c923441fcffa52a373b8cac5f Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Sat, 23 May 2026 16:38:56 +0530 Subject: [PATCH 4/5] ntdll: Gate Android exec-section privatization on arm64ec. 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 --- dlls/ntdll/unix/virtual.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/dlls/ntdll/unix/virtual.c b/dlls/ntdll/unix/virtual.c index 78155c40e35c..f0539b73beac 100644 --- a/dlls/ntdll/unix/virtual.c +++ b/dlls/ntdll/unix/virtual.c @@ -3219,7 +3219,7 @@ static IMAGE_BASE_RELOCATION *process_relocation_block( char *page, IMAGE_BASE_R } -#ifdef __ANDROID__ +#if defined(__ANDROID__) && defined(__aarch64__) /*********************************************************************** * remap_exec_anon * @@ -3320,12 +3320,15 @@ static NTSTATUS map_image_into_view( struct file_view *view, const WCHAR *filena return status; /* Windows refuses to load in that case too */ } -#ifdef __ANDROID__ +#if defined(__ANDROID__) && defined(__aarch64__) /* Proactive privatization for the whole flat image (see comment on * the per-section path) -- detecting EACCES via set_vprot failure is - * unreliable when an LD_PRELOAD shim strips PROT_EXEC. */ - remap_exec_anon( ptr, total_size, - get_unix_prot( VPROT_COMMITTED | VPROT_READ | VPROT_WRITECOPY | VPROT_EXEC ) ); + * unreliable when an LD_PRELOAD shim strips PROT_EXEC. Gated on + * arm64ec: only that build executes PE bytes as native ARM hardware + * code, so only it needs RX on relocated file mappings. */ + if (is_arm64ec()) + remap_exec_anon( ptr, total_size, + get_unix_prot( VPROT_COMMITTED | VPROT_READ | VPROT_WRITECOPY | VPROT_EXEC ) ); #endif /* set the image protections */ set_vprot( view, ptr, total_size, VPROT_COMMITTED | VPROT_READ | VPROT_WRITECOPY | VPROT_EXEC ); @@ -3483,7 +3486,7 @@ static NTSTATUS map_image_into_view( struct file_view *view, const WCHAR *filena if (sec->Characteristics & IMAGE_SCN_MEM_WRITE) vprot |= VPROT_WRITECOPY; if (sec->Characteristics & IMAGE_SCN_MEM_EXECUTE) vprot |= VPROT_EXEC; -#ifdef __ANDROID__ +#if defined(__ANDROID__) && defined(__aarch64__) /* Android W^X (SELinux execmod) refuses PROT_EXEC on a modified * private file mapping, which is exactly what a relocated PE .text * is. Detecting that via set_vprot() failure is unreliable -- LD_PRELOAD @@ -3492,8 +3495,10 @@ static NTSTATUS map_image_into_view( struct file_view *view, const WCHAR *filena * Privatize executable sections into anonymous memory PROACTIVELY, * before the final mprotect: anon memory only needs execmem, so the * subsequent set_vprot() succeeds on its own merits without needing - * to detect a failure that may have been hidden. */ - if (vprot & VPROT_EXEC) + * to detect a failure that may have been hidden. Gated on arm64ec: + * only that build executes PE bytes as native ARM hardware code, so + * only it needs RX on relocated file mappings. */ + if ((vprot & VPROT_EXEC) && is_arm64ec()) remap_exec_anon( ptr + sec->VirtualAddress, size, get_unix_prot( vprot ) ); #endif From 9e09eea062ebffd76c29a7473ef5b75830a9f847 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Sat, 23 May 2026 16:47:01 +0530 Subject: [PATCH 5/5] ntdll: Drop redundant __aarch64__ gate, keep is_arm64ec(). 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 --- dlls/ntdll/unix/virtual.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dlls/ntdll/unix/virtual.c b/dlls/ntdll/unix/virtual.c index f0539b73beac..0b19d07b2504 100644 --- a/dlls/ntdll/unix/virtual.c +++ b/dlls/ntdll/unix/virtual.c @@ -3219,7 +3219,7 @@ static IMAGE_BASE_RELOCATION *process_relocation_block( char *page, IMAGE_BASE_R } -#if defined(__ANDROID__) && defined(__aarch64__) +#ifdef __ANDROID__ /*********************************************************************** * remap_exec_anon * @@ -3320,7 +3320,7 @@ static NTSTATUS map_image_into_view( struct file_view *view, const WCHAR *filena return status; /* Windows refuses to load in that case too */ } -#if defined(__ANDROID__) && defined(__aarch64__) +#ifdef __ANDROID__ /* Proactive privatization for the whole flat image (see comment on * the per-section path) -- detecting EACCES via set_vprot failure is * unreliable when an LD_PRELOAD shim strips PROT_EXEC. Gated on @@ -3486,7 +3486,7 @@ static NTSTATUS map_image_into_view( struct file_view *view, const WCHAR *filena if (sec->Characteristics & IMAGE_SCN_MEM_WRITE) vprot |= VPROT_WRITECOPY; if (sec->Characteristics & IMAGE_SCN_MEM_EXECUTE) vprot |= VPROT_EXEC; -#if defined(__ANDROID__) && defined(__aarch64__) +#ifdef __ANDROID__ /* Android W^X (SELinux execmod) refuses PROT_EXEC on a modified * private file mapping, which is exactly what a relocated PE .text * is. Detecting that via set_vprot() failure is unreliable -- LD_PRELOAD