AX notes · Incident

16 KB page size support: a blue tint that only NDK 27 and 29 produced, and 12 .so files measured again

In our video app, 16 KB alignment came from one linker flag, not from the NDK upgrade that came first, and that upgrade is where a blue color regression came in.

By SugarMount Android development 9 min read Last updated

Six of the twelve arm64 native libraries in SugarAlbum, the ones we build ourselves, were 4 KB aligned (0x1000). Commit aedf831 on January 3, 2026 rebuilt them with NDK 26.3 at 16 KB (0x4000). Along the way, builds made with NDK 27.0 and 29.0 put a blue cast on photo transitions. On October 27, 2026 we rebuilt the libnative_filter source (two C files) with four NDK versions: NDK 25.1, which the commit lists as "16KB unsupported", gave 0x4000 once the linker flag was in.

Sources: the SugarAlbum git history (c3e09b1, d73db50 to 7efd9c1, aedf831) and the work notes committed with it, .so alignment values rechecked with llvm-readelf on 2026-10-27, and the official Android Developers documentation.

Contents (9)
  1. The three .so files rebuilt for targetSdk 36 were still 4 KB aligned
  2. NDK 27.0 and 29.0 builds tinted photo transitions blue
  3. NDK 26.3 ignores APP_SUPPORT_FLEXIBLE_PAGE_SIZES
  4. The six prebuilt FFmpeg libraries pass the LOAD check and fail the RELRO formula
  5. Keeping NDK 25.1 and adding the linker flag would have worked
  6. CMake builds and NDK r28 or later need a different procedure
  7. Checking with readelf twice and the same set of photos
  8. Not done yet: a 16 KB emulator run and a per-module -O3 check
  9. FAQ

The three .so files rebuilt for targetSdk 36 were still 4 KB aligned

SugarAlbum turns a set of photos into a video. At aedf831 the app has 763 Java files and 32 Kotlin files, and the video and filter work runs in C/C++ libraries built with ndk-build. We raised targetSdk from 33 to 34 on July 15, 2024 (c3e09b1) and from 34 to 36 between August 27 and September 1, 2025 (d73db50 to 7efd9c1). On August 31 (3e7e099) we rebuilt the three arm64 libraries of the multimediaFramework module (libBeatTracker, libPixelCanvas, libPixelUtils) without telling the linker about page size, and the other three (libnative_filter, libFFmpegProcessor, libKwpFFmpegMuxer) were still the November 2022 builds. So right before aedf831, all six had LOAD segments aligned to 0x1000.

Google announced on the Android Developers Blog on May 8, 2025 that starting November 1, 2025, new apps and updates targeting Android 15 or higher must support 16 KB pages. The current page size guide (updated 2026-10-16) says that starting February 1, 2027, updates without 16 KB support can't be released. Our repo notes say a Play Console warning asked for a fix "by May 2025". That date appears in neither official source, and the warning text itself isn't saved anywhere in the repo.

The January work was done with Claude Code; the commit carries its Co-Authored-By line. A work note committed alongside it, ffmpeg.md, records the first move: "After: NDK 27.0.12077973. Reason: the APP_SUPPORT_FLEXIBLE_PAGE_SIZES flag is only supported in NDK r27 and later." The final commit used 26.3, and ffmpeg.md went in still saying 27.0.

Step diagram: in August 2025 three .so files are rebuilt for targetSdk 36, and all six stay at 0x1000; in January 2026 the NDK goes to 27.0; 27.0 and 29.0 tint photo transitions blue; 26.3 plus the linker flag gives 0x4000; the October recheck shows NDK 25.1 also reaches 0x4000 with the flag.
After the August 2025 rebuild all six were still 0x1000; in January 2026, NDK 26.3 with the linker flag made them 0x4000.

NDK 27.0 and 29.0 builds tinted photo transitions blue

The commit message has a three-line NDK table: 25.x does not support 16 KB, 26.3 works (adopted), 27.0 and 29.0 show "a blue tint bug with -O3 optimization". progress.md, in the same commit, describes the symptom as a bluish cast during photo transitions, places it in the PixelUtils and libyuv code of the multimediaFramework module, and notes that an -O0 build looks normal. All five modules in that module's Android.mk (libYUV, libMasp, PixelCanvas, PixelUtils, BeatTracker) compile with -O3.

No automated check caught it. The only tests in the repo are the two placeholder tests Android Studio generates (ExampleUnitTest and ExampleInstrumentedTest); nothing compares colors or rendered frames. NDK 27.0 and 29.0 both built cleanly and both produced aligned libraries. The tint showed up only when someone ran the app and watched a transition.

progress.md names the cause as "a Clang -O3 optimization bug". The only evidence on record is that -O0 looks fine. The bundled compilers are clang 17.0.2 in NDK 26.3, 18.0.1 in 27.0 and 21.0.0 in 29.0, so the tint lines up with clang 18 and later. A clean -O0 build is also what you'd see if our own code had undefined behavior that the newer optimizer treats differently, and nobody narrowed it down to a function.

The same commit tidied the libyuv NEON lines in Android.mk, so we checked that too. libPixelUtils.so exports 198 symbols with NEON in the name both before and after the commit. The NEON code was already in the old build, so it isn't the variable.

NDK 26.3 ignores APP_SUPPORT_FLEXIBLE_PAGE_SIZES

The final commit sets alignment in three places: android.ndk.maxPageSize=16384 in gradle.properties, APP_SUPPORT_FLEXIBLE_PAGE_SIZES := true in three Application.mk files, and LOCAL_LDFLAGS += -Wl,-z,max-page-size=16384 in six Android.mk modules. To see which one did the work, we searched each NDK's build/core for the flag name. It isn't in 25.1 or 26.3; it's only in build-binary.mk of 27.0 and 29.0. In 27.0, a true value adds max-page-size=16384 to the link. In 29.0, 16 KB is the default and a false value sets it back to 4096.

Then we rebuilt the libnative_filter source from aedf831 with each NDK and read the LOAD alignment with llvm-readelf -lW on 2026-10-27.

NDKLinker flag in Android.mkarm64-v8a LOAD alignment
25.1.8937393no0x1000
25.1.8937393yes0x4000
26.3.11579264no (Application.mk flag only)0x1000
26.3.11579264yes (as committed)0x4000
27.0.12077973no (Application.mk flag only)0x4000
29.0.14206865no0x4000

Under 26.3 the 16 KB alignment came from the Android.mk linker flag; the Application.mk line did nothing. The same flag gave 0x4000 under 25.1. The commit's "NDK 25.x: 16KB unsupported" was never tested with the flag in place, and it follows from the first assumption that the feature needs r27. The official guide's "Android NDK r27 and lower" section also gets 16 KB alignment from the max-page-size and common-page-size linker flags and never mentions APP_SUPPORT_FLEXIBLE_PAGE_SIZES.

The six prebuilt FFmpeg libraries pass the LOAD check and fail the RELRO formula

progress.md left six prebuilt FFmpeg libraries (libavcodec-56, libavfilter-5, libavformat-56, libavutil-54, libswresample-1, libswscale-3) marked as needing a separate 16 KB rebuild. Their LOAD segments were already aligned to 0x10000 (64 KB) before the commit. 64 KB is a multiple of 16 KB, so they pass the guide's rule that no LOAD segment may be below 2**14.

The guide has a second check. (VirtAddr + MemSiz) of the GNU_RELRO segment should divide evenly by 0x4000. If it doesn't, and RELRO shares a LOAD segment with writable data that follows it (.data, .bss), the app can crash at runtime with a segmentation fault. A LOAD segment holding only RELRO data, followed by a writable LOAD that starts on a separate 16 KB page, is a case where the formula doesn't apply; the guide's example of that layout is still 16 KB compliant. We ran all twelve arm64 libraries from aedf831 through that formula.

LibraryRELRO end % 0x4000Result
libnative_filter, libBeatTracker, libFFmpegProcessor, libKwpFFmpegMuxer0x0pass
libPixelCanvas, libPixelUtils0x1000formula doesn't apply (RELRO-only LOAD)
six prebuilt FFmpeg libraries0x1000 to 0x3000RELRO shares a LOAD with .data/.bss

In libavcodec-56, the writable LOAD segment is 0x257f00 bytes in memory and only its first 0xfbf8 bytes are RELRO, which is the shape the guide uses as its crash example. The commit added only max-page-size; the guide's examples also pass common-page-size=16384. Our six self-built libraries passed without it. The repo has FFmpeg's source and build scripts, but nothing records which flags produced the .so files it ships. ffmpeg.md says -Wl,-z,max-page-size=16384 was added to ADDI_LDFLAGS in both build scripts; in this commit the two scripts were only moved, and ADDI_LDFLAGS on line 28 of build_arm64-v8a.sh doesn't have that flag.

Comparison: the commit records say NDK 25 is unsupported, 26.3 is adopted, FFmpeg needs a rebuild and the cause is a Clang -O3 bug; the recheck shows NDK 25.1 reaches 0x4000 with the linker flag, 26.3 ignores the Application.mk flag, FFmpeg is LOAD 0x10000 but its RELRO remainders are 0x1000 to 0x3000, and the cause is unconfirmed.
The six FFmpeg libraries are LOAD 0x10000, but their RELRO remainders are 0x1000 to 0x3000.

Keeping NDK 25.1 and adding the linker flag would have worked

There were three ways to go in January. We pinned NDK 26.3 and wrote "do not use NDK 27 or later" into the notes.

Don't upgrade the NDK first. Put the linker flag into the NDK you already use and check the alignment with readelf. If you have another reason to move to a newer NDK, do it as a separate change and compare rendered output against the previous build.

CMake builds and NDK r28 or later need a different procedure

This is an ndk-build project (Android.mk, Application.mk). With CMake, the guide puts the flags in target_link_options, and the Android.mk line is irrelevant. NDK r28 and later align to 16 KB by default, so the flag isn't needed at all; our 29.0 build gave 0x4000 on arm64 with no flag.

There's no basis for saying NDK 27 will do the same to other code. We didn't check 32-bit (armeabi-v7a) libraries; the guide's requirement covers 64-bit devices.

Checking with readelf twice and the same set of photos

  1. For each arm64-v8a .so that ships in the APK or AAB, read the LOAD alignment. The last column must be 0x4000 or larger.
    llvm-readelf -lW libfoo.so | grep -E "LOAD|GNU_RELRO"
  2. If there's a GNU_RELRO line, add VirtAddr and MemSiz and take the remainder mod 0x4000. If it isn't zero, check whether the LOAD segment holding RELRO also holds .data/.bss (its MemSiz is much larger than RELRO's).
  3. Before changing the NDK, add both flags from the guide to Android.mk in your current NDK and repeat step 1.
    LOCAL_LDFLAGS += -Wl,-z,max-page-size=16384 -Wl,-z,common-page-size=16384
  4. APP_SUPPORT_FLEXIBLE_PAGE_SIZES is read only by NDK r27 and later. On r26 or older, that line alone does nothing.
  5. Delete the .so files in jniLibs and rebuild every time you change the NDK or the flags. Our notes record that Android Studio's Clean Project doesn't remove jniLibs, so the build script deletes them first.
  6. Export a video from the same photos with the old build and the new one, and watch the transitions side by side.
  7. If you ship uncompressed .so files, confirm Android Gradle Plugin 8.5.1 or later and run zipalign -v -c -P 16 4 app.apk. SugarAlbum uses AGP 8.12.0.

Another Play Console warning we traced is in Play Console said our DEX was 39% optimized. The culprit was a keep rule for an SDK we never used. The app itself is on the SugarAlbum page.

Not done yet: a 16 KB emulator run and a per-module -O3 check

Video export has to run end to end on a 16 KB page emulator image to see whether the FFmpeg libraries crash on RELRO. If they do, FFmpeg needs a rebuild with common-page-size=16384. We still don't know which module the tint comes from; one way to narrow it down is to take the five multimediaFramework modules off -O3 one at a time. The NDK 27.0 in ffmpeg.md should be corrected to 26.3.

FAQ

Do I need NDK r27 or later to support 16 KB pages?

Not for the alignment itself. With -Wl,-z,max-page-size=16384 in Android.mk, NDK 25.1 rebuilt libnative_filter.so (arm64) with 0x4000 LOAD alignment (measured 2026-10-27). r27 is only needed for the one-line APP_SUPPORT_FLEXIBLE_PAGE_SIZES approach.

How do I check whether a .so is 16 KB aligned?

Run llvm-readelf -lW or llvm-objdump -p and confirm every LOAD segment is aligned to 0x4000 (2**14) or more. If GNU_RELRO shares a LOAD segment with writable data, also check that (VirtAddr + MemSiz) % 0x4000 is zero.

Can I keep a prebuilt .so that is 64 KB aligned?

It passes the LOAD alignment check. SugarAlbum's six FFmpeg libraries are 0x10000 and pass that check, but their RELRO remainders are 0x1000 to 0x3000.

This note is written from values SugarMount measured itself; an AI coding agent was used to draft it.

All notes on Android development · All AX notes