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>
415 lines
13 KiB
Python
415 lines
13 KiB
Python
#!/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()
|