Repository navigation
Conversation
The cosmetics system was C++ only, so a host that dlopen's libBerryAPI.so - a launcher's native preloader - could not drive it without linking the whole C++ surface. berry/cosmetic_c.h closes that gap the same way berry/replay_c.h did for the replay: a plain C struct of function pointers obtained from BERRY_GetCosmeticApi(), with a size/abi_version guard the host checks before calling a pointer, and a named status on every call that can fail. The surface covers reload_packs, the catalog (list/registry_count), the local loadout (equip/unequip/equipped/clear/canonical), presets and stats. It also carries the player-model plumbing: set_player_model_hook installs the detour on the host's per-build player-render target and binds a host-supplied bone reader, which is what makes a cosmetic resolve to a real rig. Without it the render path fails closed and draws nothing. BerryConfigC grew cosmetic_pack_directory and cosmetic_asset_directory, so a host can point the client at its packs through the C ABI; the global BERRY_ABI_VERSION moves 8 -> 9 (the world-seam layout is unchanged, sizeof stays 112). BerryClient gains the public ReloadCosmeticPacks / InstallPlayerModelHook / ClearPlayerModelHook that both the attach path and the C ABI use, so the two cannot drift. Verified: make test green (incl. the new tests/test_cosmetic_c.cpp and the extended tests/dlopen_test.c); the five BERRY_* symbols export in the shared library.
The world seam reported the global BERRY_ABI_VERSION, so any config-struct growth moved the seam's ABI number too and the preloader (which pins kHostSeamAbiVersion) would refuse a seam whose layout had not changed. The seam struct now carries BERRY_SEAM_ABI_VERSION, an independent number bumped only when BerrySeamC itself changes. Its value stays 8, so the preloader's pinned 8 keeps matching; the global version is free to grow for config-only reasons.
The player-model hook test passed `int hookTarget = 0` as the detour target. The shim decodes the target's prologue as x86-64 code, and reading past the four-byte stack variable is a stack-buffer-overflow that ASan aborts on. It also depended on an anonymous mapping being within a rel32 of the detour and trampoline, which the shim's jmp needs. Hook a real function in this module instead. The prologue is a normal function, the distance is guaranteed to fit, and the test now actually installs and clears the hook rather than skipping on a refused target.
The vendored config enabled MBEDTLS_AESCE_C, but only the trimmed client stack is vendored and library/aesce.c is not one of its units. aes.c guards its Armv8-A fast path on that option, so an arm64 build referenced the mbedtls_aesce_* symbols with nothing to satisfy them: the arm64 test job failed to link, and the shipping .so would carry the same unresolved symbols (a jump to address 0 the first time AES ran on an arm64 device with the crypto extension). The portable AES in aes.c is used instead. tools/check-mbedtls.py now keeps the option off, and PROVENANCE.md records why. Also stop the check-sigcheck self-test from tripping over the compiler's own bytes: the pattern is the 16-byte ELF identification header, not the bare 4-byte magic, which clang embeds as an immediate in its generated loader checks and so appears twice in a clang-built binary.
Three independent defects, each of which the g++ job either masked or never reached: - check-sigcheck pinned the ELF OSABI byte (e_ident[7]) to 00, but the g++ toolchain writes 03 (GNU/Linux). Wildcard that byte so the 16-byte identification pattern matches either toolchain. - LanCosmeticTransport::Magic() returned cosmeticfrag::kMagic, a 4-byte array with no terminator; the test builds a std::string from it, which clang read past the end. Return a NUL-terminated static, as the sibling transports do. - test_packet_capture called its fake receive function directly, so clang inlined the body and the call never reached the patched prologue. Mark it noinline and reach it through a volatile pointer. - async.http_get cast the raw double timeout to long before the call, so a NaN argument was UB at the call site even though HttpGetBlocking clamps. Guard the cast.
Lynxifyy
merged commit Nov 10, 2026
28cb0f8
into
lynxifyy/berryapi-android-build-fix
7 checks passed
Lynxifyy
added a commit
that referenced
this pull request
Nov 10, 2026
* fix(android): make the NDK build compile and target the API floor Two defects kept tools/build-android.sh from producing a library, both only visible on the on-device build (the host build never set the flag and never compiled for Android): - COMMON_FLAGS passed -fno-exceptions, but include/berry/hook_chain.h wraps each chain handler in try/catch to isolate a faulting handler. The translation unit failed with "cannot use 'try' with exceptions disabled". Exceptions are now enabled to match the host build, so the fault-isolation path compiles. - core/lifecycle/crash.cpp guarded backtrace(3) on __linux__, but on Android it is only declared from API 33 while the build targets API 24, so the symbol was undeclared. The guard now requires glibc, or a non-Android Linux, or Android API >= 33; below that the crash report still carries the signal, the fault address and the last log line. Verified: tools/build-android.sh now emits libBerryAPI.so (AArch64) exporting the full ABI-8 C surface (BERRY_SetWorldSeam / BERRY_GetReplayApi / BERRY_GetGovernorApi and the struct-size/abi-version queries). * feat(cosmetic): add a C ABI for cosmetics and the player-model hook (#21) * feat(cosmetic): add a C ABI for cosmetics and the player-model hook The cosmetics system was C++ only, so a host that dlopen's libBerryAPI.so - a launcher's native preloader - could not drive it without linking the whole C++ surface. berry/cosmetic_c.h closes that gap the same way berry/replay_c.h did for the replay: a plain C struct of function pointers obtained from BERRY_GetCosmeticApi(), with a size/abi_version guard the host checks before calling a pointer, and a named status on every call that can fail. The surface covers reload_packs, the catalog (list/registry_count), the local loadout (equip/unequip/equipped/clear/canonical), presets and stats. It also carries the player-model plumbing: set_player_model_hook installs the detour on the host's per-build player-render target and binds a host-supplied bone reader, which is what makes a cosmetic resolve to a real rig. Without it the render path fails closed and draws nothing. BerryConfigC grew cosmetic_pack_directory and cosmetic_asset_directory, so a host can point the client at its packs through the C ABI; the global BERRY_ABI_VERSION moves 8 -> 9 (the world-seam layout is unchanged, sizeof stays 112). BerryClient gains the public ReloadCosmeticPacks / InstallPlayerModelHook / ClearPlayerModelHook that both the attach path and the C ABI use, so the two cannot drift. Verified: make test green (incl. the new tests/test_cosmetic_c.cpp and the extended tests/dlopen_test.c); the five BERRY_* symbols export in the shared library. * fix(seam): give the world seam its own ABI version The world seam reported the global BERRY_ABI_VERSION, so any config-struct growth moved the seam's ABI number too and the preloader (which pins kHostSeamAbiVersion) would refuse a seam whose layout had not changed. The seam struct now carries BERRY_SEAM_ABI_VERSION, an independent number bumped only when BerrySeamC itself changes. Its value stays 8, so the preloader's pinned 8 keeps matching; the global version is free to grow for config-only reasons. * fix(cosmetic): hook a real function in the C ABI test, not a stack int The player-model hook test passed `int hookTarget = 0` as the detour target. The shim decodes the target's prologue as x86-64 code, and reading past the four-byte stack variable is a stack-buffer-overflow that ASan aborts on. It also depended on an anonymous mapping being within a rel32 of the detour and trampoline, which the shim's jmp needs. Hook a real function in this module instead. The prologue is a normal function, the distance is guaranteed to fit, and the test now actually installs and clears the hook rather than skipping on a refused target. * fix(mbedtls): drop MBEDTLS_AESCE_C; the unit it needs is not vendored The vendored config enabled MBEDTLS_AESCE_C, but only the trimmed client stack is vendored and library/aesce.c is not one of its units. aes.c guards its Armv8-A fast path on that option, so an arm64 build referenced the mbedtls_aesce_* symbols with nothing to satisfy them: the arm64 test job failed to link, and the shipping .so would carry the same unresolved symbols (a jump to address 0 the first time AES ran on an arm64 device with the crypto extension). The portable AES in aes.c is used instead. tools/check-mbedtls.py now keeps the option off, and PROVENANCE.md records why. Also stop the check-sigcheck self-test from tripping over the compiler's own bytes: the pattern is the 16-byte ELF identification header, not the bare 4-byte magic, which clang embeds as an immediate in its generated loader checks and so appears twice in a clang-built binary. * fix(ci): green the clang and sanitize host jobs Three independent defects, each of which the g++ job either masked or never reached: - check-sigcheck pinned the ELF OSABI byte (e_ident[7]) to 00, but the g++ toolchain writes 03 (GNU/Linux). Wildcard that byte so the 16-byte identification pattern matches either toolchain. - LanCosmeticTransport::Magic() returned cosmeticfrag::kMagic, a 4-byte array with no terminator; the test builds a std::string from it, which clang read past the end. Return a NUL-terminated static, as the sibling transports do. - test_packet_capture called its fake receive function directly, so clang inlined the body and the call never reached the patched prologue. Mark it noinline and reach it through a volatile pointer. - async.http_get cast the raw double timeout to long before the call, so a NaN argument was UB at the call site even though HttpGetBlocking clamps. Guard the cast. --------- Co-authored-by: Lynxifyy <297587416+Lynxifyy@users.noreply.github.com> --------- Co-authored-by: Lynxifyy <297587416+Lynxifyy@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A C ABI for the cosmetics system, so a host that
dlopenslibBerryAPI.so(a launcher's native preloader) can drive it without linking BerryAPI's C++ surface.Until now
CosmeticServiceand the device render path were C++ only.berry/cosmetic_c.hcloses that gap the same wayberry/replay_c.hclosed it for the replay: a plain C struct of function pointers fromBERRY_GetCosmeticApi(), with asize/abi_versionguard the host checks before calling a pointer and a named status on every call that can fail.The surface
Why the player-model hook is in the same ABI
A cosmetic resolves no bones unless the embedder installs a player-model hook, because the per-build player-render target is a signature-scan result the host owns (BerryAPI does not resolve it internally).
set_player_model_hook(target, reader)installs the detour on that target and binds a host-supplied bone reader, so the surface is usable rather than existing-and-drawing-nothing. Without it the render path fails closed.Config
BerryConfigCgrewcosmetic_pack_directoryandcosmetic_asset_directory, so a host can point the client at its packs through the C ABI. That is a public config layout change, soBERRY_ABI_VERSIONmoves 8 → 9. The world-seam struct layout is unchanged (sizeof(BerrySeamC)stays 112); only the number it reports moves with the global, matching how v3–v8 bumped it for config growth. The cosmetic surface itself has its ownBERRY_COSMETIC_ABI_VERSION(1).Shared code path
BerryClientgains publicReloadCosmeticPacks/InstallPlayerModelHook/ClearPlayerModelHook, andBuildCosmeticsnow calls them, so the attach path and the runtime C ABI path cannot drift.Fail-closed contract
BERRY_GetCosmeticApi()returns null before attach.Test evidence
make testgreen, including the newtests/test_cosmetic_c.cpp(catalog/loadout/presets/stats round-trip, every fail-closed case, and the hook refused-without-target / installed-with-target / cleared cycle).tests/dlopen_test.cextended todlsymthe cosmetic surface: refused before attach, self-consistent after.nm -Don the builtlibBerryAPI.so(host and arm64-v8a) shows the fiveBERRY_*cosmetic symbols exported; the arm64-v8a NDK build is green.Stacking
Based on
lynxifyy/berryapi-android-build-fix(the NDK build fix), because this branch builds on that fix. Merge that first, then this one.