From 2b729ee66da4fbd8baaf095e5b5e3dda4889972f Mon Sep 17 00:00:00 2001 From: Wang Yang <1430725+wysaid@users.noreply.github.com> Date: Tue, 28 Jul 2026 23:32:11 +1000 Subject: [PATCH] fix 16KB RELRO release validation --- .github/workflows/release.yml | 4 + CHANGELOG.md | 10 +- README.md | 12 +- library/src/main/jni/CMakeLists.txt | 7 +- tools/validate_aar_elf.py | 182 ++++++++++++++++++++++++++++ 5 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 tools/validate_aar_elf.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c157ce52..87b1c59f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -329,6 +329,10 @@ jobs: echo "✅ All AAR artifacts packaged ($AAR_COUNT files)" echo "📦 Final artifacts:" ls -lh "$ARTIFACTS_DIR/" + + - name: Validate AAR native ELF layouts + run: | + python3 tools/validate_aar_elf.py /tmp/release-artifacts/*.aar - name: Generate artifact checksums run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index e7323909..56cd2541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,13 +14,19 @@ Versions follow [Semantic Versioning](https://semver.org/). Entries are ordered - **feat:** Add `setZoomRatio(float)` API to `ICameraProvider`, `CameraXProvider`, and `Camera1Provider` (#566) - **feat:** Add `CameraXProvider` — runtime-selectable camera backend with automatic Camera2 / CameraX fallback (#565) +- **fix:** Rebuild 16KB artifacts with NDK r27c / LLD 18 so `.relro_padding` keeps + `PT_GNU_RELRO` inside its writable `PT_LOAD` mapping on 4KB-page-size devices, + fully resolving the remaining `dlopen` `ENOMEM` failure from #562. - **ci:** Release workflow overhaul — draft releases, master validation, direct Maven commit, NDK r27c/r29 (#564) --- ## [3.1.2] — 2026-02-21 -- **fix:** Remove redundant `-Wl,-z,relro,-z,now` linker flags from CMakeLists.txt to fix `UnsatisfiedLinkError` on 16KB-page-size builds (e.g. Samsung S21+ / Android 14). Android API 23+ enforces full RELRO by default; the explicit flags caused `PT_GNU_RELRO MemSiz` to exceed the mapped RW LOAD segment on 16KB-aligned builds, triggering an `mprotect()` failure with `ENOMEM`. (#563) +- **fix attempt:** Remove redundant `-Wl,-z,relro,-z,now` linker flags from + CMakeLists.txt for #562. This did not fully resolve the issue because LLD 17 + enables RELRO by default and could still emit a `PT_GNU_RELRO` range beyond + its writable `PT_LOAD` mapping when the runtime page size was 4KB. --- @@ -287,4 +293,4 @@ Pre-release snapshot using NDK 23. Not a stable release. ## [1.0.0] — 2014-09-04 -First stable release. Minor bugs may remain. \ No newline at end of file +First stable release. Minor bugs may remain. diff --git a/README.md b/README.md index bddabea8..d5976622 100644 --- a/README.md +++ b/README.md @@ -29,25 +29,29 @@ dependencies { // Page size: 4KB (default) // Architectures: armeabi-v7a, arm64-v8a, x86, x86_64 // Full-featured with FFmpeg bundled - implementation 'org.wysaid:gpuimage-plus:3.1.2' + implementation 'org.wysaid:gpuimage-plus:3.2.0' // Page size: 16KB // Architectures: armeabi-v7a, arm64-v8a, x86, x86_64 // Full-featured with FFmpeg bundled - implementation 'org.wysaid:gpuimage-plus:3.1.2-16k' + implementation 'org.wysaid:gpuimage-plus:3.2.0-16k' // Page size: 4KB (default) // Architectures: armeabi-v7a, arm64-v8a, x86, x86_64 // Image-only version (no video features or FFmpeg) - implementation 'org.wysaid:gpuimage-plus:3.1.2-min' + implementation 'org.wysaid:gpuimage-plus:3.2.0-min' // Page size: 16KB // Architectures: armeabi-v7a, arm64-v8a, x86, x86_64 // Image-only version (no video features or FFmpeg) - implementation 'org.wysaid:gpuimage-plus:3.1.2-16k-min' + implementation 'org.wysaid:gpuimage-plus:3.2.0-16k-min' } ``` +> **Important:** The `3.1.2-16k` variants were linked with LLD 17 and can fail to load on +> 4KB-page-size devices with `can't enable GNU RELRO protection ... Out of memory`. +> Use `3.2.0` or newer; its 16KB variants are compatible with both 4KB and 16KB devices. + > To compile other versions of ffmpeg, see: ## Build diff --git a/library/src/main/jni/CMakeLists.txt b/library/src/main/jni/CMakeLists.txt index 2eca05d0..54ade792 100644 --- a/library/src/main/jni/CMakeLists.txt +++ b/library/src/main/jni/CMakeLists.txt @@ -83,8 +83,9 @@ target_compile_definitions(CGE PUBLIC ANDROID_NDK=1 CGE_LOG_TAG=\"libCGE\" CGE_T # Add -O3 in non-Debug mode target_compile_options(CGE PRIVATE $<$>:-O3>) -# Note: -Wl,-z,relro,-z,now is intentionally omitted here, fixing issue: -# Android API 23+ enforces full RELRO by default; no explicit linker hardening flags are needed. +# RELRO remains enabled by the Android NDK toolchain by default. NDK r27+ includes +# LLD's .relro_padding support so 16KB-aligned binaries remain loadable on 4KB +# page-size devices. See: target_link_libraries(CGE PUBLIC GLESv2 @@ -128,4 +129,4 @@ add_library(CGEExt SHARED ${CGE_EXT_SRC}) target_include_directories(CGEExt PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/custom) target_link_libraries(CGEExt PRIVATE - CGE) \ No newline at end of file + CGE) diff --git a/tools/validate_aar_elf.py b/tools/validate_aar_elf.py new file mode 100644 index 00000000..27f0cda0 --- /dev/null +++ b/tools/validate_aar_elf.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Validate page alignment and GNU RELRO coverage in native AAR libraries.""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +import tempfile +import zipfile +from dataclasses import dataclass +from pathlib import Path + +PAGE_SIZE_4K = 4096 +PAGE_SIZE_16K = 16384 + +PROGRAM_HEADER_RE = re.compile( + r"^\s*(LOAD|GNU_RELRO)\s+" + r"(0x[0-9a-f]+)\s+" # Offset + r"(0x[0-9a-f]+)\s+" # VirtAddr + r"(0x[0-9a-f]+)\s+" # PhysAddr + r"(0x[0-9a-f]+)\s+" # FileSiz + r"(0x[0-9a-f]+)\s+" # MemSiz + r".*?\s+(0x[0-9a-f]+)\s*$", # Align + re.IGNORECASE, +) + + +@dataclass(frozen=True) +class Segment: + kind: str + virtual_address: int + memory_size: int + alignment: int + + @property + def end(self) -> int: + return self.virtual_address + self.memory_size + + +def align_down(value: int, alignment: int) -> int: + return value // alignment * alignment + + +def align_up(value: int, alignment: int) -> int: + return (value + alignment - 1) // alignment * alignment + + +def read_program_headers(shared_library: Path) -> list[Segment]: + result = subprocess.run( + ["readelf", "-lW", str(shared_library)], + check=False, + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise RuntimeError(result.stderr.strip() or "readelf failed") + + segments: list[Segment] = [] + for line in result.stdout.splitlines(): + match = PROGRAM_HEADER_RE.match(line) + if match is None: + continue + segments.append( + Segment( + kind=match.group(1).upper(), + virtual_address=int(match.group(3), 16), + memory_size=int(match.group(6), 16), + alignment=int(match.group(7), 16), + ) + ) + return segments + + +def validate_shared_library( + aar: Path, entry_name: str, shared_library: Path, require_16k: bool +) -> list[str]: + try: + segments = read_program_headers(shared_library) + except RuntimeError as error: + return [f"{aar.name}:{entry_name}: {error}"] + + load_segments = [segment for segment in segments if segment.kind == "LOAD"] + relro_segments = [ + segment for segment in segments if segment.kind == "GNU_RELRO" + ] + errors: list[str] = [] + + if not load_segments: + return [f"{aar.name}:{entry_name}: no PT_LOAD segments found"] + + if require_16k: + for load in load_segments: + if load.alignment < PAGE_SIZE_16K: + errors.append( + f"{aar.name}:{entry_name}: PT_LOAD alignment " + f"{load.alignment} is less than {PAGE_SIZE_16K}" + ) + + for relro in relro_segments: + relro_page_start = align_down(relro.virtual_address, PAGE_SIZE_4K) + relro_page_end = align_up(relro.end, PAGE_SIZE_4K) + covering_load = next( + ( + load + for load in load_segments + if align_down(load.virtual_address, PAGE_SIZE_4K) + <= relro_page_start + and align_up(load.end, PAGE_SIZE_4K) >= relro_page_end + ), + None, + ) + if covering_load is None: + errors.append( + f"{aar.name}:{entry_name}: 4KB-rounded PT_GNU_RELRO range " + f"{relro_page_start:#x}-{relro_page_end:#x} exceeds its " + "mapped PT_LOAD pages" + ) + + return errors + + +def validate_aar(aar: Path) -> list[str]: + require_16k = "-16k" in aar.stem + errors: list[str] = [] + + if not aar.is_file(): + return [f"{aar}: file not found"] + + with zipfile.ZipFile(aar) as archive, tempfile.TemporaryDirectory() as temp_dir: + native_entries = sorted( + ( + info + for info in archive.infolist() + if info.filename.startswith("jni/") + and info.filename.endswith(".so") + ), + key=lambda info: info.filename, + ) + if not native_entries: + return [f"{aar.name}: no native libraries found"] + + extraction_root = Path(temp_dir) + for entry in native_entries: + shared_library = Path(archive.extract(entry, extraction_root)) + errors.extend( + validate_shared_library( + aar, entry.filename, shared_library, require_16k + ) + ) + + if not errors: + alignment = ( + "16KB LOAD alignment and 4KB RELRO coverage" + if require_16k + else "4KB RELRO coverage" + ) + print(f"✓ {aar.name}: {alignment} valid") + return errors + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Validate native ELF layouts in published AAR files." + ) + parser.add_argument("aars", nargs="+", type=Path) + args = parser.parse_args() + + errors: list[str] = [] + for aar in args.aars: + errors.extend(validate_aar(aar)) + + if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())