CVE-2026-28594 AI-generated analysis
platform/frameworks/base, android.util.MemoryIntArray (Java + core/jni/android_util_MemoryIntArray.cpp); platform/system/core, libcutils ashmem-dev.cpp
Makes MemoryIntArray pass one explicit element count from Java to both nativeOpen and nativeClose so mmap and munmap always use the same length instead of re-querying the fd's ashmem size, and hardens libcutils ashmem_valid() so a memfd is only accepted as an ashmem replacement when it carries both F_SEAL_GROW and F_SEAL_SHRINK.
Root cause
In core/jni/android_util_MemoryIntArray.cpp, android_util_MemoryIntArray_open called ashmem_get_size_region(fd) to choose the mmap length, and android_util_MemoryIntArray_close called ashmem_get_size_region(fd) again to choose the munmap length. Nothing tied the two values together. If the size reported for the fd was different at close time (the fd's backing region changed size, or a different fd was in mFd), munmap(ashmemAddr, newSize) unmapped a range that did not match the original mapping. Separately, libcutils is_memfd_fd() (used by ashmem_valid()) accepted any fd whose /proc/self/fd link started with "/memfd:", regardless of file seals. A memfd whose size could still change was therefore treated as a valid ashmem fd, which the commit message says opens time-of-check to time-of-use issues.
How the fix works
- platform/frameworks/base core/java/android/util/MemoryIntArray.java: both constructors now compute mSize = nativeSize(mFd) first and pass it to nativeOpen(mFd, mIsOwner, mSize). close() passes mSize to nativeClose(mFd, mMemoryAddr, mSize). The native declarations change to nativeOpen(int, boolean, int) and nativeClose(int, long, int), with a comment requiring the same size for both.
- platform/frameworks/base core/jni/android_util_MemoryIntArray.cpp: android_util_MemoryIntArray_open and android_util_MemoryIntArray_close take a jint size and compute ashmemSize = arraySize(size) (sizeof(std::atomic_int) * size) instead of calling ashmem_get_size_region(fd). The existing 'bad ashmem size' check on ashmemSize <= 0 is kept. The owner-path placement-new now uses the passed size. The new helpers arraySize/arrayNrElms are used by create and size. JNI registration is updated to "(IZI)J" and "(IJI)V".
- platform/system/core libcutils/ashmem-dev.cpp: after the "/memfd:" prefix check, is_memfd_fd() now calls fcntl(fd, F_GET_SEALS). It returns false if that call fails or if F_SEAL_GROW and F_SEAL_SHRINK are not both set, and only otherwise returns true. Because ashmem_valid() returns 1 when is_memfd_fd() is true, unsealed memfds are no longer treated as valid ashmem fds.
- platform/system/core libcutils/ashmem_test.cpp: adds AshmemValidTest.UnsealedMemfdTest, which asserts that ashmem_valid() is false for an unsealed memfd and for memfds with only one of the two seals.
- platform/frameworks/base core/tests/utiltests: adds testMmapAndMunmapSizeConsistency plus nativeMremap/nativeIsRangeMapped/nativeMunmap helpers. The test swaps in a larger fd before close() and checks that only the original page is unmapped and that an adjacent probe page survives.
Completeness
- Covered: the munmap length in android_util_MemoryIntArray_close is now fixed at open time through the Java-held mSize. Neither a size change on the fd nor a different fd in mFd can change the unmapped range.
- Covered: libcutils is_memfd_fd(), and so ashmem_valid(), rejects memfds that are not sealed against both growing and shrinking. Which other libcutils entry points depend on is_memfd_fd() is not shown in the material.
- Not changed: mSize is still obtained once from nativeSize(mFd) (ashmem_get_size_region) in both constructors, including the Parcel constructor that receives the fd from another process. The mmap length now comes from that single query, but the array length itself is still provided by the fd.
- Not changed: nativeGet/nativeSet and the Java-side bounds handling are not in the diff, and the material does not show them.
- Only is_memfd_fd() changed in ashmem-dev.cpp. The legacy /dev/ashmem character-device path (__ashmem_is_ashmem) is untouched by this fix.
Behavior changes
- ashmem_valid() now returns false for memfds lacking F_SEAL_GROW and F_SEAL_SHRINK, or when F_GET_SEALS fails (logged with ALOGE). Previously ashmem_valid() accepted any memfd.
- MemoryIntArray native method signatures changed (nativeOpen (IZI)J, nativeClose (IJI)V). Java and JNI must be updated together.
- Constructor ordering changed: nativeSize(mFd) is now called before nativeOpen, so a size-query failure happens before any mapping is created.
- nativeOpen/nativeClose now throw IOException 'bad ashmem size' when the Java-provided size gives a non-positive byte length, rather than when the fd's queried size is non-positive.
Per major
- Android 14: Not affected per the bulletin. 6/8 lines of the patched region are present at android-security-14.0.0_r29; no fix is recorded.
- Android 15: Not affected per the bulletin. 8/8 lines of the patched region are present at android-security-15.0.0_r17; no fix is recorded.
- Android 16: Not affected per the bulletin for the android-security-16.0.0 line (16-qpr2 is listed separately). 8/8 lines of the patched region are present at android-security-16.0.0_r8; no fix is recorded.
- Android 17: Same fix: resolved via bug_id at android-security-17.0.0_r1 with frameworks/base a44880c82aac plus system/core 6297cab2c921 and 2de25eeaec55 (all cherry-picks with Merged-In).
Unknowns
- Which privileged processes accept MemoryIntArray parcels from less-privileged callers. The index search returned only generic close() callers, nothing specific.
- Why Android 14, 15 and 16.0 are not listed as affected. The per-major line counts alone do not show whether the vulnerable code is present there.
- Whether libcutils ashmem entry points other than ashmem_valid() call is_memfd_fd() and therefore also get the seal requirement.
- Whether nativeGet/nativeSet have Java-side bounds checks against mSize (not in the diff).