Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down Expand Up @@ -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.
First stable release. Minor bugs may remain.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <https://github.com/wysaid/FFmpeg-Android.git>

## Build
Expand Down
7 changes: 4 additions & 3 deletions library/src/main/jni/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 $<$<NOT:$<CONFIG:Debug>>:-O3>)

# Note: -Wl,-z,relro,-z,now is intentionally omitted here, fixing issue: <https://github.com/wysaid/android-gpuimage-plus/issues/562>
# 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: <https://github.com/wysaid/android-gpuimage-plus/issues/562>

target_link_libraries(CGE PUBLIC
GLESv2
Expand Down Expand Up @@ -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)
CGE)
182 changes: 182 additions & 0 deletions tools/validate_aar_elf.py
Original file line number Diff line number Diff line change
@@ -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())
Loading