009: Closure wave 3 deblob, handoff, and optional wave4/malloc bacons.

Ship script gates wave 4 behind AXP_DEBLOB_WAVE4, documents secrets cert path,
and adds deblob appliers plus tests; docs capture Swissquote fix and parity.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
sunfish-adminandCursor committed 2026-10-09 17:31:01 +02:00
1 parent deaaf42caa
commit 3f941e8451
64 files changed
+6270 -86

No files matched your search

@@ -0,0 +1,414 @@
#!/usr/bin/env python3
"""012: Graphene hardened_malloc on LOS 23.2, the AXP GRAPHENE_MALLOC set.
Source of the integration is GrapheneOS platform_bionic branch 16:
64-bit libc uses hardened_malloc, 32-bit stays Scudo, and SurfaceFlinger
plus camera executables stay on Scudo. AXP documents that camera exception
as applying to every camera executable, which on sunfish is
/vendor/bin/hw/android.hardware.camera.provider@2.7-service-google rather
than the newer Pixel apex path.
Library pin: GrapheneOS/hardened_malloc 1d7fc7ffe0f8 (branch 14, 2025-04-03),
the newest branch that is not the Android 17 line. device_has_arm_mte is
removed because LOS soong has no such product variable and sunfish has no MTE.
Class region size is 2GiB, as DivestOS/AXP set it, because the sunfish
kernel has a 39-bit address space (GOLDEN: VA_BITS=39 with hardened_malloc).
system/core init.rc raises vm.max_map_count to 1048576, same as GrapheneOS 16.
frameworks/av no longer sets RLIMIT_AS on this tree, so that AXP/Pie hunk
does not apply.
"""
from __future__ import annotations
import os
import pathlib
import subprocess
import sys
MARKER = "SUNFISH_A16_012_HARDENED_MALLOC"
PIN = "1d7fc7ffe0f8068a32ab2ba33df7515e7bad3f5f"
REPO = "https://github.com/GrapheneOS/hardened_malloc.git"
REGION_48BIT = "-DCONFIG_CLASS_REGION_SIZE=34359738368"
REGION_39BIT = "-DCONFIG_CLASS_REGION_SIZE=2147483648"
HERE = pathlib.Path(__file__).resolve().parent
TREE = pathlib.Path(os.environ.get("ANDROID_BUILD_TOP", "/srv/rom/src/lineage"))
def must_replace(path: pathlib.Path, old: str, new: str, label: str) -> None:
text = path.read_text()
if new in text:
print(f"already applied: {path} ({label})")
return
if old not in text:
sys.exit(f"anchor missing for {label} in {path}")
path.write_text(text.replace(old, new, 1))
print(f"patched {label}: {path}")
def rewrite_region_comment(bp: pathlib.Path) -> None:
text = bp.read_text()
stale = f'{REGION_39BIT}", // 32GiB'
fresh = f'{REGION_39BIT}", // 2GiB, 39-bit VA (DivestOS/AXP)'
if stale not in text:
return
bp.write_text(text.replace(stale, fresh))
print(f"region comment: {bp}")
def ensure_library() -> None:
dest = TREE / "external/hardened_malloc"
if (dest / ".git").is_dir():
head = subprocess.check_output(
["git", "-C", dest, "rev-parse", "HEAD"], text=True
).strip()
bp_text = (dest / "Android.bp").read_text()
if head == PIN and MARKER in bp_text and REGION_39BIT in bp_text:
rewrite_region_comment(dest / "Android.bp")
print(f"hardened_malloc already at {PIN}")
return
subprocess.check_call(["git", "-C", dest, "fetch", "--depth", "1", "origin", PIN])
subprocess.check_call(["git", "-C", dest, "checkout", "--detach", PIN])
else:
if dest.exists():
sys.exit(f"{dest} exists and is not a git checkout")
dest.parent.mkdir(parents=True, exist_ok=True)
subprocess.check_call(
["git", "clone", "--depth", "1", REPO, str(dest)]
)
subprocess.check_call(["git", "-C", dest, "fetch", "--depth", "1", "origin", PIN])
subprocess.check_call(["git", "-C", dest, "checkout", "--detach", PIN])
bp = dest / "Android.bp"
text = bp.read_text()
mte = """ device_has_arm_mte: {
cflags: ["-DHAS_ARM_MTE", "-march=armv8-a+dotprod+memtag"]
},
"""
if mte in text:
text = text.replace(mte, "")
# sunfish msm-4.14 is CONFIG_ARM64_VA_BITS=39. The 32GiB default class
# region needs 48-bit VA and aborts every 64-bit process at boot.
# DivestOS/AXP Patch.sh: sed 's/34359738368/2147483648/' Android.bp.
text = text.replace(REGION_48BIT, REGION_39BIT)
text = text.replace(
f'{REGION_39BIT}", // 32GiB',
f'{REGION_39BIT}", // 2GiB, 39-bit VA (DivestOS/AXP)',
)
bp.write_text(text)
text = bp.read_text()
if REGION_39BIT not in text:
sys.exit(f"CONFIG_CLASS_REGION_SIZE is not 2GiB in {bp}")
if MARKER not in text:
text = text.replace(
"cc_library {\n",
f"cc_library {{\n // {MARKER}: sunfish has no MTE; LOS soong has no device_has_arm_mte.\n",
1,
)
bp.write_text(text)
head = subprocess.check_output(
["git", "-C", dest, "rev-parse", "HEAD"], text=True
).strip()
if head != PIN:
sys.exit(f"hardened_malloc HEAD {head} != {PIN}")
print(f"hardened_malloc {PIN}")
def patch_bionic() -> None:
wrapper = TREE / "bionic/libc/bionic/h_malloc_wrapper.cpp"
src = HERE / "h_malloc_wrapper.cpp"
if not wrapper.exists() or wrapper.read_text() != src.read_text():
wrapper.write_text(src.read_text())
print(f"installed {wrapper}")
must_replace(
TREE / "bionic/libc/Android.bp",
"""cc_defaults {
name: "libc_native_allocator_defaults",
whole_static_libs: [
"libscudo",
],
cflags: [
"-DUSE_SCUDO",
],
header_libs: ["gwp_asan_headers"],
product_variables: {
malloc_low_memory: {
cflags: ["-UUSE_SCUDO"],
whole_static_libs: [
"libjemalloc5",
"libc_jemalloc_wrapper",
],
exclude_static_libs: [
"libscudo",
],
},
},
}
""",
f"""cc_defaults {{
// {MARKER}
name: "libc_native_allocator_defaults",
header_libs: ["gwp_asan_headers"],
whole_static_libs: ["libscudo"],
// GrapheneOS 16: hardened_malloc on 64-bit, Scudo on 32-bit.
multilib: {{
lib32: {{
cflags: ["-DUSE_SCUDO"],
}},
lib64: {{
srcs: ["bionic/h_malloc_wrapper.cpp"],
cflags: [
"-DH_MALLOC_PREFIX",
"-DUSE_H_MALLOC",
"-DUSE_SCUDO",
],
whole_static_libs: [
"libhardened_malloc",
],
}},
}},
}}
""",
"allocator-defaults",
)
must_replace(
TREE / "bionic/libc/bionic/malloc_common.h",
"""#else // __has_feature(hwaddress_sanitizer)
#if defined(USE_SCUDO)
""",
f"""#else // __has_feature(hwaddress_sanitizer)
// {MARKER}
#ifdef __LP64__
#ifndef USE_H_MALLOC
#error missing USE_H_MALLOC
#endif
#include "h_malloc.h"
#define Malloc(function) h_ ## function
__BEGIN_DECLS
int h_malloc_info(int options, FILE* fp);
__END_DECLS
#if defined(USE_SCUDO)
#include "scudo.h"
void InitNativeAllocatorDispatch(libc_globals* globals);
#endif
#define BOTH_H_MALLOC_AND_SCUDO
#else // 32-bit
#if defined(USE_SCUDO)
""",
"malloc-header",
)
header = TREE / "bionic/libc/bionic/malloc_common.h"
text = header.read_text()
tail = """#define Malloc(function) je_ ## function
#endif
#endif
const MallocDispatch* NativeAllocatorDispatch();
"""
tail_new = """#define Malloc(function) je_ ## function
#endif
#endif // __LP64__
#endif // __has_feature(hwaddress_sanitizer)
const MallocDispatch* NativeAllocatorDispatch();
"""
if tail_new not in text:
if tail not in text:
sys.exit(f"header close anchor missing in {header}")
header.write_text(text.replace(tail, tail_new, 1))
must_replace(
TREE / "bionic/libc/bionic/malloc_common.cpp",
"""const MallocDispatch* NativeAllocatorDispatch() {
return &__libc_malloc_default_dispatch;
}
""",
f"""#if defined(BOTH_H_MALLOC_AND_SCUDO)
// {MARKER}: camera executables and surfaceflinger stay on Scudo.
#define ScudoMalloc(function) scudo_ ## function
static constexpr MallocDispatch __scudo_malloc_dispatch __attribute__((unused)) = {{
ScudoMalloc(calloc),
ScudoMalloc(free),
ScudoMalloc(mallinfo),
ScudoMalloc(malloc),
ScudoMalloc(malloc_usable_size),
ScudoMalloc(memalign),
ScudoMalloc(posix_memalign),
#if defined(HAVE_DEPRECATED_MALLOC_FUNCS)
ScudoMalloc(pvalloc),
#endif
ScudoMalloc(realloc),
#if defined(HAVE_DEPRECATED_MALLOC_FUNCS)
ScudoMalloc(valloc),
#endif
ScudoMalloc(malloc_iterate),
ScudoMalloc(malloc_disable),
ScudoMalloc(malloc_enable),
ScudoMalloc(mallopt),
ScudoMalloc(aligned_alloc),
ScudoMalloc(malloc_info),
}};
static const MallocDispatch* native_allocator_dispatch;
void InitNativeAllocatorDispatch(libc_globals* globals) {{
bool hardened_impl = true;
if (globals->flags & GLOBAL_FLAG_DISABLE_HARDENED_MALLOC) {{
hardened_impl = false;
}} else if (getenv("DISABLE_HARDENED_MALLOC") != nullptr) {{
hardened_impl = false;
}}
const MallocDispatch* table = hardened_impl ?
&__libc_malloc_default_dispatch :
&__scudo_malloc_dispatch;
if (!hardened_impl) {{
globals->malloc_dispatch_table = __scudo_malloc_dispatch;
globals->current_dispatch_table = &globals->malloc_dispatch_table;
globals->default_dispatch_table = &globals->malloc_dispatch_table;
}}
native_allocator_dispatch = table;
}}
const MallocDispatch* NativeAllocatorDispatch() {{
return native_allocator_dispatch;
}}
#else
const MallocDispatch* NativeAllocatorDispatch() {{
return &__libc_malloc_default_dispatch;
}}
#endif
""",
"dispatch",
)
must_replace(
TREE / "bionic/libc/bionic/malloc_common_dynamic.cpp",
"""void MallocInitImpl(libc_globals* globals) {
char prop[PROP_VALUE_MAX];
""",
f"""void MallocInitImpl(libc_globals* globals) {{
#if defined(BOTH_H_MALLOC_AND_SCUDO)
// {MARKER}
InitNativeAllocatorDispatch(globals);
#endif
char prop[PROP_VALUE_MAX];
""",
"malloc-init",
)
must_replace(
TREE / "bionic/libc/private/bionic_globals.h",
""" MallocDispatch malloc_dispatch_table;
};
""",
f""" MallocDispatch malloc_dispatch_table;
// {MARKER}
int flags;
}};
#define GLOBAL_FLAG_DISABLE_HARDENED_MALLOC 1
""",
"globals",
)
init = TREE / "bionic/libc/bionic/libc_init_dynamic.cpp"
text = init.read_text()
if '#include <string.h>' not in text:
if '#include <stdlib.h>' not in text:
sys.exit(f"stdlib include missing in {init}")
text = text.replace(
"#include <stdlib.h>\n",
"#include <stdlib.h>\n#include <string.h>\n#include <unistd.h>\n",
1,
)
helper = f"""
// {MARKER}: GrapheneOS disables hardened_malloc for surfaceflinger.
// AXP expands the camera exception to every camera executable. sunfish runs
// /vendor/bin/hw/android.hardware.camera.provider@2.7-service-google.
static void init_hardened_malloc_exclusions(libc_globals* globals) {{
char exe_path[500];
ssize_t n = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
if (n <= 0) {{
globals->flags = 0;
return;
}}
exe_path[n] = '\\0';
if (strcmp(exe_path, "/system/bin/surfaceflinger") == 0 ||
strstr(exe_path, "camera") != nullptr) {{
globals->flags = GLOBAL_FLAG_DISABLE_HARDENED_MALLOC;
}} else {{
globals->flags = 0;
}}
}}
"""
# __libc_preinit_impl must stay noinline (stack protector guard setup).
noinline = "__attribute__((noinline))\n"
anchor = noinline + "static void __libc_preinit_impl() {"
misplaced = noinline + helper + "static void __libc_preinit_impl() {"
if misplaced in text:
text = text.replace(misplaced, helper + anchor, 1)
if "init_hardened_malloc_exclusions" not in text:
if anchor not in text:
sys.exit(f"preinit anchor missing in {init}")
text = text.replace(anchor, helper + anchor, 1)
if anchor not in text:
sys.exit(f"__libc_preinit_impl lost noinline in {init}")
old_mutate = " __libc_globals.mutate(__libc_init_malloc);\n"
new_mutate = """ __libc_globals.mutate([](libc_globals* globals) {
init_hardened_malloc_exclusions(globals);
__libc_init_malloc(globals);
});
"""
if "init_hardened_malloc_exclusions(globals)" not in text:
if old_mutate not in text:
sys.exit(f"mutate anchor missing in {init}")
text = text.replace(old_mutate, new_mutate, 1)
init.write_text(text)
print(f"patched exclusions: {init}")
def patch_init_rc() -> None:
must_replace(
TREE / "system/core/rootdir/init.rc",
" symlink /sys/class/block/${dev.mnt.rootdisk.data} /dev/sys/block/by-name/rootdisk\n",
" symlink /sys/class/block/${dev.mnt.rootdisk.data} /dev/sys/block/by-name/rootdisk\n\n"
f" # {MARKER}: GrapheneOS 16 vm.max_map_count for hardened_malloc\n"
" write /proc/sys/vm/max_map_count 1048576\n",
"max-map-count",
)
def main() -> None:
if not (TREE / "bionic/libc/Android.bp").is_file():
sys.exit(f"not an Android tree: {TREE}")
ensure_library()
patch_bionic()
patch_init_rc()
print(f"done: {MARKER}")
if __name__ == "__main__":
main()
@@ -0,0 +1,51 @@
#include <errno.h>
#include <malloc.h>
#include <sys/param.h>
#include <unistd.h>
#include <private/MallocXmlElem.h>
#include "h_malloc.h"
__BEGIN_DECLS
int h_malloc_info(int options, FILE* fp);
__END_DECLS
int h_malloc_info(int options, FILE* fp) {
if (options != 0) {
errno = EINVAL;
return -1;
}
fflush(fp);
int fd = fileno(fp);
MallocXmlElem root(fd, "malloc", "version=\"jemalloc-1\"");
// Dump all of the large allocations in the arenas.
for (size_t i = 0; i < h_mallinfo_narenas(); i++) {
struct mallinfo mi = h_mallinfo_arena_info(i);
if (mi.hblkhd != 0) {
MallocXmlElem arena_elem(fd, "heap", "nr=\"%d\"", i);
{
MallocXmlElem(fd, "allocated-large").Contents("%zu", mi.ordblks);
MallocXmlElem(fd, "allocated-huge").Contents("%zu", mi.uordblks);
MallocXmlElem(fd, "allocated-bins").Contents("%zu", mi.fsmblks);
size_t total = 0;
for (size_t j = 0; j < h_mallinfo_nbins(); j++) {
struct mallinfo mi = h_mallinfo_bin_info(i, j);
if (mi.ordblks != 0) {
MallocXmlElem bin_elem(fd, "bin", "nr=\"%d\"", j);
MallocXmlElem(fd, "allocated").Contents("%zu", mi.ordblks);
MallocXmlElem(fd, "nmalloc").Contents("%zu", mi.uordblks);
MallocXmlElem(fd, "ndalloc").Contents("%zu", mi.fordblks);
total += mi.ordblks;
}
}
MallocXmlElem(fd, "bins-total").Contents("%zu", total);
}
}
}
return 0;
}