From 940209a64db2aad5e9d611a114a88bfa786f1f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Thu, 6 Aug 2026 22:39:49 +0200 Subject: [PATCH 1/2] build: bump NDK to r28 for 16 KB page-size compliance Google Play rejects app updates without 16 KB page-size support since Nov 2025. The aw-server-rust native libs were built with NDK r25, which links LOAD segments 4 KB-aligned (verified: 0x1000). NDK r28+ links with -z max-page-size=16384 by default for all Android targets. Also extends scripts/check-jnilibs.py to fail CI when 64-bit libs have LOAD alignment below 0x4000, so an NDK downgrade can't silently regress compliance (mirrors Google's check_elf_alignment semantics: every LOAD segment must be >=16 KB aligned). --- .github/workflows/build.yml | 3 +- mobile/build.gradle | 2 +- scripts/check-jnilibs.py | 95 +++++++++++++++++++++++++++---------- 3 files changed, 72 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24d50c88..d76eb08e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,8 @@ on: # Usage: gh workflow run build.yml --repo ActivityWatch/aw-android --ref refs/tags/v env: - NDK_VERSION: '25.2.9519653' + # r28+: links native libs with 16 KB page alignment by default (Play requirement since Nov 2025) + NDK_VERSION: '28.2.13676358' NODE_VERSION: '16' JAVA_VERSION: '17' diff --git a/mobile/build.gradle b/mobile/build.gradle index b67c87ea..184729a9 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'kotlin-android' android { compileSdk 35 - ndkVersion "25.2.9519653" + ndkVersion "28.2.13676358" defaultConfig { applicationId "net.activitywatch.android" diff --git a/scripts/check-jnilibs.py b/scripts/check-jnilibs.py index 3f349ddf..27054b75 100755 --- a/scripts/check-jnilibs.py +++ b/scripts/check-jnilibs.py @@ -7,6 +7,36 @@ ABIS = ["arm64-v8a", "armeabi-v7a", "x86", "x86_64"] LIB_NAME = "libaw_server.so" +# Google Play requires 16 KB page-size support for 64-bit native libs +# (enforced for updates since Nov 2025). NDK r28+ links with 16 KB +# max-page-size by default; this guards against regressing to an older NDK. +PAGE_ALIGNED_ABIS = {"arm64-v8a", "x86_64"} +REQUIRED_LOAD_ALIGN = 0x4000 + + +def min_load_align(f, header, e_class): + """Return the min p_align across PT_LOAD program headers (None if none).""" + if e_class == 1: # 32-bit + e_phoff = struct.unpack("= fsize: - print( - f"CORRUPT {path}: section header past EOF " - f"(e_shoff=0x{e_shoff:x}, size={fsize})", - file=sys.stderr, - ) - return False + if e_shoff != 0 and e_shoff >= fsize: + print( + f"CORRUPT {path}: section header past EOF " + f"(e_shoff=0x{e_shoff:x}, size={fsize})", + file=sys.stderr, + ) + return False + + align = min_load_align(f, header, e_class) + if abi in PAGE_ALIGNED_ABIS: + if align is None or align < REQUIRED_LOAD_ALIGN: + print( + f"BAD_ALIGN {path}: LOAD align " + f"{'none' if align is None else hex(align)} < " + f"{hex(REQUIRED_LOAD_ALIGN)} (16 KB pages required; " + f"build with NDK r28+)", + file=sys.stderr, + ) + return False - print(f"OK {abi}/{LIB_NAME} ({fsize:,} bytes)") + align_str = "none" if align is None else hex(align) + print(f"OK {abi}/{LIB_NAME} ({fsize:,} bytes, LOAD align {align_str})") return True From 2f250553a9d2a102f204c07b1c30abc474f3c4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Thu, 6 Aug 2026 22:47:59 +0200 Subject: [PATCH 2/2] fix(check-jnilibs): also validate libaw_sync.so, bound program-header reads Review findings: the gate only checked the hardcoded libaw_server.so while the Makefile also packages libaw_sync.so; and a truncated ELF declaring program headers past EOF crashed struct.unpack instead of reporting a diagnostic. Require both known libs, validate any other packaged *.so, and bounds-check phdr offsets/reads. --- scripts/check-jnilibs.py | 68 ++++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/scripts/check-jnilibs.py b/scripts/check-jnilibs.py index 27054b75..9e0cc486 100755 --- a/scripts/check-jnilibs.py +++ b/scripts/check-jnilibs.py @@ -1,11 +1,14 @@ #!/usr/bin/env python3 +import glob import os import struct import sys ABIS = ["arm64-v8a", "armeabi-v7a", "x86", "x86_64"] -LIB_NAME = "libaw_server.so" +# Libs the Makefile always packages; MISSING is an error for these. Any other +# *.so found in an ABI dir is validated too (ELF sanity + alignment). +REQUIRED_LIBS = ["libaw_server.so", "libaw_sync.so"] # Google Play requires 16 KB page-size support for 64-bit native libs # (enforced for updates since Nov 2025). NDK r28+ links with 16 KB @@ -14,19 +17,36 @@ REQUIRED_LOAD_ALIGN = 0x4000 -def min_load_align(f, header, e_class): - """Return the min p_align across PT_LOAD program headers (None if none).""" +def min_load_align(f, path, fsize, header, e_class): + """Return (ok, min p_align across PT_LOAD headers or None if none).""" if e_class == 1: # 32-bit e_phoff = struct.unpack(" fsize: + print( + f"CORRUPT {path}: program headers out of bounds " + f"(e_phoff=0x{e_phoff:x}, e_phentsize={e_phentsize}, " + f"e_phnum={e_phnum}, size={fsize})", + file=sys.stderr, + ) + return False, None align = None for i in range(e_phnum): f.seek(e_phoff + i * e_phentsize) phdr = f.read(e_phentsize) + if len(phdr) < e_phentsize: + print( + f"CORRUPT {path}: short read of program header {i}", + file=sys.stderr, + ) + return False, None p_type = struct.unpack("