CVE-2024-0033 AI-generated analysis
platform/frameworks/native (libs/binder MemoryHeapBase) and platform/system/core (libcutils ashmem-dev memfd backend)
Adds F_SEAL_GROW | F_SEAL_SHRINK to memfd-backed shared memory created by libbinder MemoryHeapBase(FORCE_MEMFD) and by the libcutils ashmem-dev memfd backend (which also now adds F_SEAL_SEAL when a region is made read-only), so a remote process holding the fd can no longer change the size of the shared region.
Root cause
Neither memfd creation path applied F_SEAL_GROW/F_SEAL_SHRINK. In libs/binder/MemoryHeapBase.cpp the FORCE_MEMFD path creates the memfd (memfd_create + ftruncate), maps it with mapfd(), and then only applied F_SEAL_FUTURE_WRITE when READ_ONLY and F_SEAL_SEAL unless MEMFD_ALLOW_SEALING_FLAG was set. A default region therefore had no size seal, and SEAL_FLAGS could be 0, in which case sealing was skipped. In libcutils/ashmem-dev.cpp, memfd_create_region did not add size seals, and memfd_set_prot_region only added F_SEAL_FUTURE_WRITE (without F_SEAL_SEAL) for read-only requests and returned 0 for a later PROT_WRITE request on an already write-protected fd. Because the fd is handed to other processes, a recipient could ftruncate the memfd and shrink the region mapped by the originating process. Per the commit messages this crashes the originating process (DoS). The bulletin rates it High/EoP.
How the fix works
- platform/frameworks/native aa98edf0ce9d, libs/binder/MemoryHeapBase.cpp: MemoryHeapBase::MemoryHeapBase(size_t size, uint32_t flags, char const* name), FORCE_MEMFD branch: SEAL_FLAGS now always includes F_SEAL_GROW | F_SEAL_SHRINK, in addition to the existing conditional F_SEAL_FUTURE_WRITE (READ_ONLY) and F_SEAL_SEAL (when MEMFD_ALLOW_SEALING_FLAG is absent). SEAL_FLAGS is therefore never 0, so fcntl(fd, F_ADD_SEALS, ...) always runs after mapfd(). The existing failure handling (munmap, mBase=nullptr, mSize=0, close(fd)) is unchanged.
- libs/binder/tests/binderMemoryHeapBaseUnitTest.cpp: the four Memfd* tests now expect F_SEAL_GROW | F_SEAL_SHRINK in F_GET_SEALS for every flag combination and assert that ftruncate(fd, 4096) fails (the regions are 8192 bytes where the test bodies are shown).
- platform/system/core 46d46dc46446, libcutils/ashmem-dev.cpp: memfd_create_region() adds fcntl(fd, F_ADD_SEALS, F_SEAL_GROW | F_SEAL_SHRINK) after its earlier error check. On failure it logs and returns -1 (fd appears to be a unique_fd, given fd.get(), so it is closed).
- libcutils/ashmem-dev.cpp: memfd_set_prot_region() now first reads fcntl(fd, F_GET_SEALS) and returns -1 if that fails. If prot includes PROT_WRITE and F_SEAL_FUTURE_WRITE is already set, it fails with errno=EINVAL to match ashmem semantics, where it used to return 0. For read-only requests it now adds F_SEAL_FUTURE_WRITE | F_SEAL_SEAL (previously only F_SEAL_FUTURE_WRITE), which prevents later seal changes.
Completeness
- Covers both memfd creation paths shown: the libbinder FORCE_MEMFD constructor and the libcutils ashmem-dev memfd backend (used when has_memfd_support() is true).
- In MemoryHeapBase the size seals are applied regardless of READ_ONLY or MEMFD_ALLOW_SEALING_FLAG, so every memfd region created by that constructor is protected. With MEMFD_ALLOW_SEALING_FLAG, F_SEAL_SEAL is still omitted by design, so recipients can add further seals; the header comment describes this as an opt-in.
- The ashmem (/dev/ashmem) fallback path in MemoryHeapBase and in ashmem-dev is untouched. The system/core commit comment says the seals are added 'to match ashmem behaviour', which implies ashmem already forbids size changes.
- The other MemoryHeapBase constructors (for example the device path) are not changed. They do not create a memfd and are outside this fix.
- On non-bionic (host) builds the FORCE_MEMFD branch is compiled out and FORCE_MEMFD is cleared, as before.
- In MemoryHeapBase.cpp the local static memfd_create_region() (lines 43-55 at r29) still only does memfd_create + ftruncate. The seals are applied in the constructor after mapfd(), before the constructor returns.
Behavior changes
- ftruncate (grow or shrink) on fds from MemoryHeapBase(FORCE_MEMFD), or from memfd-backed ashmem_create_region, now fails in every process, including the creator.
- MemoryHeapBase FORCE_MEMFD now always calls F_ADD_SEALS. If sealing fails, the region is unmapped and the fd closed. Previously sealing was skipped when SEAL_FLAGS was 0.
- memfd_create_region in libcutils now returns -1 if F_ADD_SEALS fails.
- ashmem_set_prot_region on a memfd-backed fd: requesting PROT_WRITE after the fd was made read-only now returns -1 with errno=EINVAL. Making it read-only also sets F_SEAL_SEAL, so no further seals can be added. A failing F_GET_SEALS also returns -1.
- New or changed log lines in both files.
Per major
- Android 14: Fixed. Both commits (frameworks/native aa98edf0ce9d and system/core 46d46dc46446) landed in android-security-14.0.0_r5. At android-security-14.0.0_r29 the patched MemoryHeapBase.cpp seal flags are present (lines 76-77). The resolver reports 6/8 lines of the system/core patched region present; the material does not show which lines differ.
- Android 15: Not affected per the bulletin (not listed). 8/8 lines of the patched region present at android-security-15.0.0_r17.
- Android 16: Not affected per the bulletin (not listed). 8/8 lines of the patched region present at android-security-16.0.0_r8.
- Android 17: Not affected per the bulletin (not listed). 8/8 lines of the patched region present at android-security-17.0.0_r1.
Unknowns
- Which concrete system services or IPC paths were used to reach the unsealed memfd in the reported issue. The material only gives the generic cross-process description from the commit messages.
- Why the bulletin rates this EoP/High while the commit messages describe a DoS (crash of the originating process).
- Which 2 of 8 lines of the system/core patched region differ at android-security-14.0.0_r29 (the shown context does not include that region).
- Whether MemoryHeapBaseAllocator or other index-found users of MemoryHeapBase pass FORCE_MEMFD.