From 80179abaf71652dae760acbf0c9e8462bc9348c0 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Thu, 21 May 2026 17:05:09 +0300 Subject: [PATCH] glibc-compat,uclibc-compat: dual-ABI mmap shim (fix musl-caller EINVAL) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The static mmap shims from #2000 (uclibc) and #2110 (glibc) use the signature `void *mmap(..., uint32_t offset)`. That's correct for vendor blobs built with a 32-bit off_t but silently breaks for musl-built callers that get linked into the same executable. ARM EABI places the 6th mmap arg in different stack slots depending on off_t width: vendor 32-bit off_t: offset at SP+4 (slot right after `fd`) musl 64-bit off_t: SP+4..7 is 8-byte alignment padding, offset_lo at SP+8, offset_hi at SP+12 A `uint32_t offset` formal reads SP+4 — for musl callers that's uninitialised stack padding, so the kernel rejects the resulting pgoff with EINVAL. Invisible for minimal consumers like majestic (which only delegates to libmpi); bites the moment a consumer mixes vendor MPP with a non- trivial musl-static library that issues mmap from user code. The canonical reproducer is a dvr_home demo that statically links LVGL — HI_MPI_VENC_CreateChn succeeds (vendor path) but lv_linux_fbdev fails to mmap /dev/fb0 ("failed to map framebuffer device to memory: Invalid argument") even though hifb_demo's bare-mmap path in the exact same rootfs works fine. Fix: declare mmap/mmap64 variadic, va_arg-peek BOTH candidate slots (SP+4 and SP+8) as uint32_t — uint32_t avoids the 8-byte alignment that va_arg(ap, uint64_t) would apply — then pick the slot whose value is page-aligned. For the common offset=0 case from either ABI it resolves to 0 cleanly; for vendor MMZ mmaps with a non-zero page-aligned PA at SP+4 the heuristic still picks SP+4. Failure mode: uninit padding happens to be page-aligned by coincidence (~1/4096 calls). Kernel still validates the resulting mapping, so worst case is a one-off EINVAL at startup — never silent corruption. Applied identically to both shims; the stat() translators in uclibc-compat-static.c are untouched. Refs: #1992 (audit), #1993 (cv100 stat shim), #2000 (cv100 mmap split), #2110 (V2 SoC port). Bisected against dvr_home on hi3520dv200; PROBE inside lv_hifb_disp_init showed mmap(/dev/fb0, ..., 0) returning EINVAL with the old shim and the same call returning a valid mapping once the dual-ABI peek was in place. --- .../glibc-compat/src/glibc-compat-static.c | 60 ++++++++++++++++++- .../uclibc-compat/src/uclibc-compat-static.c | 48 +++++++++++++-- 2 files changed, 101 insertions(+), 7 deletions(-) diff --git a/general/package/glibc-compat/src/glibc-compat-static.c b/general/package/glibc-compat/src/glibc-compat-static.c index 4177697046..cd4b6f5998 100644 --- a/general/package/glibc-compat/src/glibc-compat-static.c +++ b/general/package/glibc-compat/src/glibc-compat-static.c @@ -16,11 +16,44 @@ * musl's mmap process-wide, breaking musl's own internal mmap callers * (malloc, dlopen, ...). * + * Dual-ABI peek (subtle): + * + * The static lib lives in the executable, so it intercepts mmap() calls + * from BOTH the vendor blob (glibc 32-bit off_t) AND from musl-built code + * that gets linked into the same binary (e.g. LVGL's lv_linux_fbdev, or + * any static library that issues mmap from user code). ARM EABI places the + * 6th arg differently depending on off_t width: + * + * glibc 32-bit off_t: offset at SP+4 (the slot right after `fd`) + * musl 64-bit off_t: SP+4..7 is alignment padding, offset_lo at SP+8 + * + * A `void *mmap(..., uint32_t offset)` signature reads SP+4. For musl + * callers that slot holds uninitialised stack — garbage — and the kernel + * rejects the resulting pgoff with EINVAL. The bug is invisible for + * minimal vendor-only consumers (which is what PR #2110 tested) but bites + * the moment a consumer mixes vendor MPP with non-trivial musl-static + * code: e.g. a dvr_home demo that statically links LVGL fails to mmap + * /dev/fb0 even though the bare-mmap path in a simpler binary works. + * + * Fix: declare mmap/mmap64 variadic, va_arg-peek BOTH candidate slots + * (SP+4 and SP+8) as uint32_t — uint32_t avoids the 8-byte alignment that + * va_arg(ap, uint64_t) would apply on ARM — then pick the slot whose + * value is page-aligned. For the common offset=0 case from either ABI it + * resolves to 0 cleanly; for vendor MMZ mmaps with a non-zero page-aligned + * physical address at SP+4 the heuristic still picks SP+4. The only + * failure mode is musl's uninit-padding happening to be page-aligned by + * coincidence (~1/4096 calls) — and that only matters if the musl caller + * ALSO has offset=0 and the picked SP+4 value happens to be a valid + * unrelated mapping (much rarer); for any value the kernel still validates, + * so worst case is EINVAL on a single startup call, never silent corruption. + * * Pattern mirrors OpenIPC/firmware#2000 (uclibc-compat-static for the - * hi3516cv100 vendor blobs). + * hi3516cv100 vendor blobs); see also PR #2110 for the glibc V2 SoC port + * and PR #1993 for the audit tool that finds these mismatches statically. */ #define _GNU_SOURCE +#include #include #include #include @@ -30,12 +63,33 @@ #define SYS_mmap2 192 #endif -void *mmap(void *addr, size_t len, int prot, int flags, int fd, uint32_t offset) +static uint32_t pick_offset(uint32_t slot_a, uint32_t slot_b) +{ + int a_pa = (slot_a & 0xFFF) == 0; + int b_pa = (slot_b & 0xFFF) == 0; + if (a_pa && !b_pa) return slot_a; + if (b_pa) return slot_b; + return slot_a; +} + +void *mmap(void *addr, size_t len, int prot, int flags, int fd, ...) { + va_list ap; + va_start(ap, fd); + uint32_t slot_a = va_arg(ap, uint32_t); /* SP+4 — vendor ABI */ + uint32_t slot_b = va_arg(ap, uint32_t); /* SP+8 — musl ABI low */ + va_end(ap); + uint32_t offset = pick_offset(slot_a, slot_b); return (void *)syscall(SYS_mmap2, addr, len, prot, flags, fd, offset >> 12); } -void *mmap64(void *addr, size_t len, int prot, int flags, int fd, uint32_t offset) +void *mmap64(void *addr, size_t len, int prot, int flags, int fd, ...) { + va_list ap; + va_start(ap, fd); + uint32_t slot_a = va_arg(ap, uint32_t); + uint32_t slot_b = va_arg(ap, uint32_t); + va_end(ap); + uint32_t offset = pick_offset(slot_a, slot_b); return (void *)syscall(SYS_mmap2, addr, len, prot, flags, fd, offset >> 12); } diff --git a/general/package/uclibc-compat/src/uclibc-compat-static.c b/general/package/uclibc-compat/src/uclibc-compat-static.c index a52068ac94..cb0b7825cf 100644 --- a/general/package/uclibc-compat/src/uclibc-compat-static.c +++ b/general/package/uclibc-compat/src/uclibc-compat-static.c @@ -16,6 +16,7 @@ * See: https://github.com/OpenIPC/firmware/issues/1992 */ +#include #include #include #include @@ -65,24 +66,63 @@ int lstat64(const char *p, void *b) { return lstat(p, b); } int fstat64(int fd, void *b) { return fstat(fd, b); } /* ====================================================================== - * mmap -- off_t width mismatch + * mmap -- off_t width mismatch (dual-ABI peek) * * uclibc: offset is uint32_t (4 bytes, page-aligned) * musl: offset is off_t (8 bytes) * - * On ARM, this changes the calling convention. Bypass with raw syscall. + * On ARM EABI the 6th arg lands in different stack slots per off_t width: + * uclibc 32-bit: offset at SP+4 + * musl 64-bit: SP+4..7 is alignment padding, offset_lo at SP+8 + * + * The static lib intercepts mmap() for both ABIs since it sits in the + * consumer executable. A plain `uint32_t offset` signature reads SP+4 — + * fine for the uclibc vendor blob, but musl-built static code in the + * same binary (e.g. LVGL fbdev) gets uninitialised padding from SP+4 and + * the kernel rejects with EINVAL. Symptom: lv_linux_fbdev fails to mmap + * /dev/fb0 even though a simpler bare-mmap binary in the same rootfs + * works fine. + * + * Fix: declare mmap/mmap64 variadic, va_arg-peek BOTH candidate slots + * (SP+4 and SP+8) as uint32_t, pick the page-aligned one. For + * offset=0 from either ABI this resolves to 0 cleanly; for vendor MMZ + * mmaps with a non-zero page-aligned PA at SP+4 the heuristic still + * picks SP+4. Failure mode: uninit padding accidentally page-aligned + * (~1/4096 calls); kernel still validates, so worst case is a one-off + * EINVAL, never silent corruption. * ====================================================================== */ #ifndef SYS_mmap2 #define SYS_mmap2 192 #endif -void *mmap(void *addr, size_t len, int prot, int flags, int fd, uint32_t offset) +static uint32_t pick_offset(uint32_t slot_a, uint32_t slot_b) +{ + int a_pa = (slot_a & 0xFFF) == 0; + int b_pa = (slot_b & 0xFFF) == 0; + if (a_pa && !b_pa) return slot_a; + if (b_pa) return slot_b; + return slot_a; +} + +void *mmap(void *addr, size_t len, int prot, int flags, int fd, ...) { + va_list ap; + va_start(ap, fd); + uint32_t slot_a = va_arg(ap, uint32_t); /* SP+4 — vendor ABI */ + uint32_t slot_b = va_arg(ap, uint32_t); /* SP+8 — musl ABI low */ + va_end(ap); + uint32_t offset = pick_offset(slot_a, slot_b); return (void *)syscall(SYS_mmap2, addr, len, prot, flags, fd, offset >> 12); } -void *mmap64(void *addr, size_t len, int prot, int flags, int fd, uint32_t offset) +void *mmap64(void *addr, size_t len, int prot, int flags, int fd, ...) { + va_list ap; + va_start(ap, fd); + uint32_t slot_a = va_arg(ap, uint32_t); + uint32_t slot_b = va_arg(ap, uint32_t); + va_end(ap); + uint32_t offset = pick_offset(slot_a, slot_b); return (void *)syscall(SYS_mmap2, addr, len, prot, flags, fd, offset >> 12); }