Google is committed to advancing racial equity for Black communities. See how.

Execute-only Memory (XOM) for AArch64 Binaries

Executable code sections for AArch64 system binaries are by default marked execute-only (non-readable) as a hardening mitigation against just-in-time code reuse attacks. Code that mixes data and code together and code that purposefully inspects these sections (without first remapping the memory segments as readable) no longer function. Apps with a target SDK of 10 (API level 29 or higher) are impacted if the app attempts to read code sections of execute-only memory (XOM) enabled system libraries in memory without first marking the section as readable.

To fully benefit from this mitigation, both hardware and kernel support are required. Without this support, the mitigation might only be partially enforced. The Android 4.9 common kernel contains the appropriate patches to provide full support for this on ARMv8.2 devices.

Implementation

AArch64 binaries generated by the compiler assume that code and data aren't intermixed. Enabling this feature doesn't negatively affect the performance of the device.

For code that has to perform intentional memory introspection on its executable segments, it's advisable to call mprotect on the segments of code requiring inspection to allow them to be readable, then remove the readability when the inspection is complete.
This implementation causes reads into memory segments marked as execute-only to result in a segmentation fault (SEGFAULT). This might occur as a result of a bug, vulnerability, data mixed with code (literal pooling), or intentional memory introspection.

Device support and impact

Devices with earlier hardware or earlier kernels (lower than 4.9) without the required patches might not fully support or benefit from this feature. Devices without kernel support may not enforce user accesses of execute-only memory, however kernel code which explicitly checks whether a page is readable may still enforce this property, such as process_vm_readv().

The kernel flag CONFIG_ARM64_UAO must be set in the kernel to ensure that the kernel respects userland pages marked execute-only. Earlier ARMv8 devices, or ARMv8.2 devices with User Access Override (UAO) disabled, may not fully benefit from this and may still be able to read execute-only pages using syscalls.

Refactoring existing code

Code that has been ported from AArch32 might contain intermixed data and code, causing issues to arise. In many cases, fixing these issues is as simple as moving the constants to a .data section in the assembly file.

Handwritten assembly may need to be refactored to separate locally pooled constants.

Examples:

Binaries generated by the Clang compiler should have no issues with data being intermixed in code. If GNU compiler collection (GCC) generated code is included (from a static library), inspect the output binary to ensure that constants have not been pooled into code sections.

If code introspection is necessary on executable code sections, first call mprotect to mark the code readable. Then after the operation is complete, call mprotect again to mark it unreadable.

Enabling

Execute-only is enabled by default for all 64-bit binaries in the build system.

Disabling

You can disable execute-only at a module level, by an entire subdirectory tree, or globally for an entire build.

XOM can be disabled for individual modules that can't be refactored, or need to read their executable code, by setting the LOCAL_XOM and xom variables to false.

// Android.mk
LOCAL_XOM := false

// Android.bp
cc_binary { // or other module types
   ...
   xom: false,
}

If execute-only memory is disabled in a static library, the build system applies this to all dependent modules of that static library. You can override this by using xom: true,.

To disable execute-only memory in a particular subdirectory (for example, foo/bar/), pass the value to XOM_EXCLUDE_PATHS.

make -j XOM_EXCLUDE_PATHS=foo/bar

Alternatively, you can set the PRODUCT_XOM_EXCLUDE_PATHS variable in your product configuration.

You can disable execute-only binaries globally by passing ENABLE_XOM=false to your make command.

make -j ENABLE_XOM=false

Validation

There are no CTS or verification tests available for execute-only memory. You can manually verify binaries using readelf and checking the segment flags.