009: Polish bacon fixes, OTA host scripts, and migration docs.

Restore disable_configstore, EuiccDisabler and thermal CDEV guards, branding
and rebuild/sync helpers; document device roles, laptop migration, and Gitea.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
sunfish-adminandCursor committed 2026-10-16 21:06:45 +02:00
1 parent 3f941e8451
commit 2d145c2878
3193 files changed
+21264 -371

No files matched your search

+11 -4
View File
@@ -14,8 +14,9 @@
4. **Magisk preroot** — EXTENDROM-class boot/`init_boot` patch using Magisk APK
from `/srv/rom/prebuilts/magisk/` (see `docs/009-magisk-preinstall-runbook.md`).
Stub: `vendor/sunfish-a16/magisk.mk`. Attribute: Magisk upstream + sfX
extendrom Magisk patcher. **Host tree currently has no `vendor/extendrom`**
— must vendor minimal patcher or checkout extendrom before first Magisk bacon.
extendrom Magisk patcher. **Shipped on LAB (2026-10):** Magisk 30.7 preroot
via host EXTENDROM at `/srv/rom/references/extendrom/` (wired by the ship
bacon). Do not treat the old “blocked on T011” note as current.
5. **Bundled apps** — `/srv/rom/prebuilts/axp-apps/` + `apps.mk` (Aurora,
Droidify, Fossify Gallery). Symlink APKs into `vendor/sunfish-a16/prebuilt/`.
6. **Private DNS presets** — `apply_009_private_dns_providers.py` (GOLDEN list).
@@ -31,6 +32,10 @@
`vendor/sunfish-a16/patches/009-kernel-hardening/apply_stage1.sh`
(AXP Graphene/AH allowlist). Priority: AXP → LOS → Iodé → optional
Graphene; no Divest hunt; no extra location lockdown.
11. **2026-10-16 polish** (ship bacon) — `apply_009_euicc_disabler_guard.py`,
`apply_009_thermal_cdev_log.py`; wave4
`apply_009_drop_lab_only_commands.py` must **keep/restore**
`disable_configstore`. Details: `docs/009-lab-polish-20261016.md`.
## Product includes
@@ -54,5 +59,7 @@ Wire Magisk-resurrection product from `vendor/sunfish-a16/` into
| Vanilla baseline | `/srv/rom/artifacts/003-vanilla/20260914-lineage-23.2-sunfish/` |
| Optional KSU boots | `/srv/rom/artifacts/007-kernelsu/` (non-ship) |
**Gap:** `vendor/extendrom` is **not** checked out in the lineage tree yet.
Magisk bacon blocked on T011 (symlink/checkout + PREROOT or slim wire).
**Host wiring (current):** EXTENDROM Magisk patcher lives under
`/srv/rom/references/extendrom/` and is invoked by
`scripts/009-bacon-axp-stage1-combo4-cve-hi-releasekeys.sh`. Magisk APK pin:
`/srv/rom/prebuilts/magisk/Magisk-v30.7.apk`.
@@ -33,6 +33,7 @@ KEYGUARD = (
BASE / "packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java"
)
QSTILE = BASE / "packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java"
SYSTEM_SERVER = BASE / "services/java/com/android/server/SystemServer.java"
XML_OLD = """ <permission name="android.permission.REAL_GET_TASKS"/>
<permission name="android.permission.REQUEST_NETWORK_SCORES"/>
@@ -65,6 +66,42 @@ IMPORT_NEW = (
)
REQUIRED_MARKERS = (
"settings_reboot_after_timeout",
"android.permission.REBOOT",
"cancelDoRebootForOwnerAfterTimeoutIfEnabled();",
"class SystemServerExt",
"class WifiAutoOff",
"class BluetoothAutoOff",
"import android.bluetooth.BluetoothProfile;",
"WIFI_OFF_TIMEOUT",
"BLUETOOTH_OFF_TIMEOUT",
"isAllowedWhenLocked",
"KeyguardStateController",
)
def markers_blob() -> str:
paths = (
BASE / "core/java/android/provider/Settings.java",
XML,
KEYGUARD,
BASE / "services/core/java/com/android/server/ext/SystemServerExt.java",
BASE / "services/core/java/com/android/server/ext/WifiAutoOff.java",
BASE / "services/core/java/com/android/server/ext/BluetoothAutoOff.java",
QSTILE,
)
return "\n".join(path.read_text() for path in paths)
def markers_present() -> bool:
try:
blob = markers_blob()
except OSError:
return False
return all(item in blob for item in REQUIRED_MARKERS)
def replace_once(path: pathlib.Path, old: str, new: str) -> None:
text = path.read_text()
if new in text:
@@ -97,18 +134,43 @@ def apply_patch(name: str) -> None:
)
sys.stdout.write(result.stdout)
sys.stdout.write(result.stderr)
if result.returncode != 0 and "FAILED" in result.stderr + result.stdout:
print(f"patch_incomplete: {name}")
elif result.returncode != 0:
print(f"patch_exit_{result.returncode}: {name}")
combined = result.stdout + result.stderr
dismiss_stale_rejects()
if result.returncode != 0 or "FAILED" in combined:
if markers_present():
print(f"patch_skip_ok (markers present): {name}")
return
if (
name == "0015-System_Server_Extensions.patch"
and "com.android.server.ext.SystemServerExt.init" in SYSTEM_SERVER.read_text()
):
print(f"patch_skip_ok (SystemServerExt.init present): {name}")
return
sys.exit(f"FAIL patch {name}: {combined[-800:]}")
def dismiss_stale_rejects() -> None:
"""LOS A16 moved anchors; AXP hunks may leave .rej while the code is already in."""
rej = SYSTEM_SERVER.parent / f"{SYSTEM_SERVER.name}.rej"
if rej.is_file() and "com.android.server.ext.SystemServerExt.init" in SYSTEM_SERVER.read_text():
rej.unlink()
print("OK: dismissed stale SystemServer.java.rej")
def clean_rejects() -> None:
for path in (XML, KEYGUARD, QSTILE):
for suffix in (".rej", ".orig"):
extra = path.with_name(path.name + suffix)
if extra.exists():
extra.unlink()
rejs = list(BASE.rglob("*.rej"))
if markers_present():
for path in rejs:
path.unlink()
print(f"OK: purged stale reject {path}")
return
for path in rejs:
sys.exit(f"FAIL: reject file {path}")
def scrub_patch_artifacts() -> None:
for path in BASE.rglob("*.orig"):
path.unlink()
def main() -> None:
@@ -124,33 +186,11 @@ def main() -> None:
):
apply_patch(name)
replace_once(QSTILE, IMPORT_OLD, IMPORT_NEW)
scrub_patch_artifacts()
dismiss_stale_rejects()
clean_rejects()
required = [
"settings_reboot_after_timeout",
"android.permission.REBOOT",
"cancelDoRebootForOwnerAfterTimeoutIfEnabled();",
"class SystemServerExt",
"class WifiAutoOff",
"class BluetoothAutoOff",
"import android.bluetooth.BluetoothProfile;",
"WIFI_OFF_TIMEOUT",
"BLUETOOTH_OFF_TIMEOUT",
"isAllowedWhenLocked",
"KeyguardStateController",
]
blob = "\n".join(
path.read_text()
for path in (
BASE / "core/java/android/provider/Settings.java",
XML,
KEYGUARD,
BASE / "services/core/java/com/android/server/ext/SystemServerExt.java",
BASE / "services/core/java/com/android/server/ext/WifiAutoOff.java",
BASE / "services/core/java/com/android/server/ext/BluetoothAutoOff.java",
QSTILE,
)
)
missing = [item for item in required if item not in blob]
blob = markers_blob()
missing = [item for item in REQUIRED_MARKERS if item not in blob]
if missing:
sys.exit("missing after apply: " + ", ".join(missing))
print("done: SUNFISH_A16_009_A15_REBOOT_TIMEOUTS")
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""009: AXP visual branding from android_vendor_lineage (Disroot AXP.OS).
Copies bootanimation + vendor/lineage/overlay tree from
vendor/sunfish-a16/branding/ into the LOS tree. Adds Resurrection copy on
setup welcome where AXP/Lineage would show a generic OS name.
"""
from __future__ import annotations
import pathlib
import re
import shutil
import sys
MARKER = "SUNFISH_A16_009_AXP_BRANDING"
TREE = pathlib.Path("/srv/rom/src/lineage")
BRANDING = TREE / "vendor/sunfish-a16/branding"
BOOTANIM_SRC = BRANDING / "bootanimation"
BOOTANIM_DST = TREE / "vendor/lineage/bootanimation"
GEN_BOOTANIM_SH = BOOTANIM_DST / "gen-bootanimation.sh"
OVERLAY_SRC = BRANDING / "overlay"
OVERLAY_DST = TREE / "vendor/lineage/overlay"
SETUP_STRINGS = TREE / "packages/apps/SetupWizard/res/values/strings.xml"
WELCOME_OLD = re.compile(
r'<string name="setup_welcome_message">.*?</string>',
re.DOTALL,
)
WELCOME_NEW = (
f" <!-- {MARKER}: AXP visual track; Resurrection suffix on welcome -->\n"
' <string name="setup_welcome_message">Welcome to AXP Resurrection</string>'
)
def _copy_file(src: pathlib.Path, dst: pathlib.Path) -> None:
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
def _sync_tree(src_root: pathlib.Path, dst_root: pathlib.Path) -> int:
if not src_root.is_dir():
sys.exit(f"missing branding dir: {src_root}")
n = 0
for src in src_root.rglob("*"):
if not src.is_file():
continue
# Android.bp under branding/ is a Soong duplicate of vendor/lineage
# modules; never leave/copy them into the sunfish-a16 tree scan path.
if src.name in ("Android.bp", "Android.mk"):
continue
rel = src.relative_to(src_root)
dst = dst_root / rel
if src.stat().st_size != (dst.stat().st_size if dst.is_file() else -1) or (
not dst.is_file()
or src.read_bytes() != dst.read_bytes()
):
_copy_file(src, dst)
n += 1
return n
def _patch_gen_bootanimation_sh() -> None:
"""LOS 23 builds bootanimation.zip via Soong gen-bootanimation.sh, not Android.mk."""
if not GEN_BOOTANIM_SH.is_file():
sys.exit(f"missing {GEN_BOOTANIM_SH}")
text = GEN_BOOTANIM_SH.read_text()
marker = "AXP_BOOTANIMATION_FULLSCREEN"
if marker in text and 'grep -q "^c 0 0 part2"' in text:
print(f"already: {GEN_BOOTANIM_SH} ({marker})")
return
if marker in text:
sys.exit(
f"{GEN_BOOTANIM_SH} has stale {marker} patch; "
"git checkout vendor/lineage/bootanimation/gen-bootanimation.sh then re-run"
)
old = (
"IMAGESCALEHEIGHT=$(expr $IMAGESCALEWIDTH / 3);\n\n"
'if [ "$PARAM_TARGET_BOOTANIMATION_HALF_RES" = "true" ]; then\n'
' IMAGEWIDTH="$(expr "$IMAGEWIDTH" / 2)"\n'
"fi\n\n"
"IMAGEHEIGHT=$(expr $IMAGEWIDTH / 3);\n"
)
new = (
f"# {marker}: 3-part AXP desc => full screen (Soong sandbox has no flag file)\n"
"AXP_FULLSCREEN=0\n"
'if grep -q "^c 0 0 part2" "$PARAM_DESC_TXT" 2>/dev/null; then\n'
" AXP_FULLSCREEN=1\n"
"fi\n"
'if [ "$AXP_FULLSCREEN" = 1 ]; then\n'
" IMAGESCALEHEIGHT=$PARAM_TARGET_SCREEN_HEIGHT\n"
"else\n"
" IMAGESCALEHEIGHT=$(expr $IMAGESCALEWIDTH / 3)\n"
"fi\n\n"
'if [ "$PARAM_TARGET_BOOTANIMATION_HALF_RES" = "true" ]; then\n'
' IMAGEWIDTH="$(expr "$IMAGEWIDTH" / 2)"\n'
"fi\n\n"
'if [ "$AXP_FULLSCREEN" = 1 ]; then\n'
" IMAGEHEIGHT=$PARAM_TARGET_SCREEN_HEIGHT\n"
"else\n"
" IMAGEHEIGHT=$(expr $IMAGEWIDTH / 3)\n"
"fi\n"
)
if old not in text:
sys.exit(f"gen-bootanimation.sh anchor missing in {GEN_BOOTANIM_SH}")
GEN_BOOTANIM_SH.write_text(text.replace(old, new, 1))
print(f"patched: {GEN_BOOTANIM_SH}")
def _patch_setup_welcome() -> None:
if not SETUP_STRINGS.is_file():
sys.exit(f"missing {SETUP_STRINGS}")
text = SETUP_STRINGS.read_text()
if MARKER in text:
print(f"already: {SETUP_STRINGS} ({MARKER})")
return
if not WELCOME_OLD.search(text):
sys.exit(f"setup_welcome_message anchor missing in {SETUP_STRINGS}")
text = WELCOME_OLD.sub(WELCOME_NEW, text, count=1)
SETUP_STRINGS.write_text(text)
print(f"patched: {SETUP_STRINGS}")
def main() -> None:
if not BOOTANIM_SRC.is_dir():
sys.exit(f"missing {BOOTANIM_SRC}")
boot_n = _sync_tree(BOOTANIM_SRC, BOOTANIM_DST)
over_n = _sync_tree(OVERLAY_SRC, OVERLAY_DST) if OVERLAY_SRC.is_dir() else 0
_patch_gen_bootanimation_sh()
_patch_setup_welcome()
print(f"OK: bootanimation files updated={boot_n} overlay files updated={over_n}")
if __name__ == "__main__":
main()
@@ -7,17 +7,22 @@
- Drops the old TARGET_UNOFFICIAL_BUILD_ID insert from lineage_sunfish.mk.
- Vendor SPL stays the LOS 23.2 2023-09-05 (AXP GOLDEN ships 2023-08-05).
- build/make/core/config.mk only tags release-keys when the default cert is
under vendor/lineage-priv/. Copies the project certs there (same keys, so
an existing install updates without a wipe).
under vendor/lineage-priv/. Copies the host-only project certs there (same
keys, so an existing install updates without a wipe). Secrets never live in
vendor/sunfish-a16.
"""
from __future__ import annotations
import os
import pathlib
import re
import shutil
import sys
TREE = pathlib.Path("/srv/rom/src/lineage")
SRC_CERTS = TREE / "vendor/sunfish-a16/certs"
SRC_CERTS = pathlib.Path(
os.environ.get("ANDROID_CERTS_DIR", "/srv/rom/secrets/android-certs")
)
DST_CERTS = TREE / "vendor/lineage-priv/keys"
OLD_ID = (
"# sunfish-a16: must precede vendor/lineage version.mk (via inherit below)\n"
@@ -38,9 +43,30 @@ VENDOR_SPL_ROLLBACK = (
)
def los_date() -> str | None:
date = os.environ.get("AXP_LOS_DATE")
pin = TREE / ".axp-los-date"
if not date and pin.is_file():
date = pin.read_text().strip()
if date and not re.fullmatch(r"\d{8}", date):
sys.exit(f"bad LOS date {date!r}, want YYYYMMDD")
return date
def main() -> None:
version = TREE / "vendor/lineage/config/version.mk"
text = version.read_text()
date = los_date()
if date:
# Build name carries the upstream LOS nightly date, not the build day.
text, n = re.subn(
r"(LINEAGE_BUILD_DATE := )(\$\(shell date -u \+%Y%m%d\)|\d{8})\n",
rf"\g<1>{date}\n",
text,
)
if n != 1:
sys.exit(f"LINEAGE_BUILD_DATE anchor missing in {version}")
version.write_text(text)
if "LINEAGE_BUILDTYPE := PRO\n" not in text:
old = " LINEAGE_BUILDTYPE := UNOFFICIAL\n"
if old not in text:
@@ -64,6 +90,14 @@ def main() -> None:
if not (SRC_CERTS / "releasekey.pk8").is_file():
sys.exit(f"missing {SRC_CERTS}/releasekey.pk8")
avb_dir = TREE / "vendor/sunfish-a16/certs"
avb_dir.mkdir(parents=True, exist_ok=True)
for name in ("avb.pem", "avb_pkmd.bin"):
src = SRC_CERTS / name
if not src.is_file():
sys.exit(f"missing {src}")
shutil.copy2(src, avb_dir / name)
DST_CERTS.mkdir(parents=True, exist_ok=True)
for src in SRC_CERTS.iterdir():
if src.name.startswith("avb"):
@@ -1,14 +1,14 @@
#!/usr/bin/env python3
"""US-S4-2 — drop Lineage Jelly + Gallery2 to match AXP.OS PRO product surface.
"""US-S4-2 — optional mk scrubber for Lineage Jelly/Gallery2 package lists.
Oracle: AXP.OS-22.2-20260324-PRO-sunfish.zip product/app has neither Jelly nor
Gallery2 (browser stub + FossifyGallery_FD live on system_ext). See
docs/009-axp-pro-product-apps-20260324.md.
Ship policy (2026-10-07+, handoff): **Jelly stays** on product; gallery is
FossifyGallery on system_ext; **Gallery2/Glimpse stay out**. That is enforced
by vendor/sunfish-a16/hardening.mk (filter-out + PRODUCT_PACKAGES += Jelly)
via magisk.mk — **not** by running this script on the OTA bacon path.
FossifyGallery already declares LOCAL_OVERRIDES_PACKAGES := Gallery Gallery2
Glimpse, but Gallery2 still landed in product/app on the 162840Z ship — same
class of failure as OpenSSH filter-out. Patch the Lineage PRODUCT_PACKAGES
add sites and add belt-and-suspenders stubs.
This file remains for one-off mk surgery if vendor/lineage re-adds Gallery2
despite hardening.mk (e.g. "Build Gallery2 until Glimpse can edit"). Do not
strip Jelly here when using the current ship gates.
Usage (from ANDROID_BUILD_TOP):
python3 vendor/sunfish-a16/patches/009-axp-los23-port/apply_009_drop_jelly_gallery2.py
@@ -7,6 +7,11 @@ dumpstate and apexd require them. Remove only those required entries.
pbtombstone stays in PRODUCT_HOST_PACKAGES; only the system copy goes.
Qualcomm diag binaries and imsrcsd go. imsdatadaemon and the other IMS
call daemons stay.
Never drop disable_configstore. It is the AOSP Soong override for
android.hardware.configstore@1.1-service (base_vendor SHIPPING_API_LEVEL_29).
Without it the HIDL HAL installs, is absent from A16 VINTF, and crash-loops
every ~5s (LAB polish 2026-10-16). Lineage device-lineage.mk must keep it.
"""
from __future__ import annotations
@@ -38,13 +43,17 @@ MAKE_DROPS: tuple[tuple[str, tuple[str, ...], tuple[str, ...]], ...] = (
"ims_rtp_daemon",
),
),
(
"device/google/sunfish/device-lineage.mk",
("disable_configstore",),
("chre_daemon_msm",),
),
)
# Restored if a prior wave4 pass emptied the Configstore stanza.
CONFIGSTORE_MK = "device/google/sunfish/device-lineage.mk"
CONFIGSTORE_NEEDLE = "disable_configstore"
CONFIGSTORE_BLOCK = """# Configstore
PRODUCT_PACKAGES += \\
disable_configstore
"""
HOST_SAFE = {"pbtombstone"}
BP_LINES: tuple[tuple[str, str], ...] = (
@@ -164,6 +173,41 @@ def apply_bp(path: Path, needle: str) -> str:
return "dropped"
def ensure_disable_configstore(path: Path) -> str:
"""Keep Lineage's override so the HIDL configstore HAL is not packaged."""
text = path.read_text()
if not _has(text, "chre_daemon_msm"):
raise SystemExit(f"FAIL: {path} lost chre_daemon_msm")
if _has(text, CONFIGSTORE_NEEDLE):
return "already"
# Prior wave4 left an empty "# Configstore / PRODUCT_PACKAGES +=" block.
empty = (
"# Configstore\n"
"PRODUCT_PACKAGES += \\\n"
"\n"
)
if empty in text:
new = text.replace(
empty,
"# Configstore\nPRODUCT_PACKAGES += \\\n disable_configstore\n\n",
1,
)
elif "# Configstore\n" in text:
raise SystemExit(f"FAIL: {path} has Configstore stanza but no restore site")
else:
# Insert before CHRE block (Lineage stock order).
marker = "# CHRE\n"
if marker not in text:
raise SystemExit(f"FAIL: {path} missing CHRE marker for Configstore insert")
new = text.replace(marker, CONFIGSTORE_BLOCK + marker, 1)
if not _has(new, CONFIGSTORE_NEEDLE):
raise SystemExit(f"FAIL: could not restore {CONFIGSTORE_NEEDLE} in {path}")
if not _has(new, "chre_daemon_msm"):
raise SystemExit(f"FAIL: edit would drop chre_daemon_msm from {path}")
path.write_text(new)
return "restored"
def main() -> int:
top = Path(os.environ.get("ANDROID_BUILD_TOP", ".")).resolve()
results: list[str] = []
@@ -173,6 +217,11 @@ def main() -> int:
print(f"FAIL: missing {path}", file=sys.stderr)
return 1
results.append(f"{apply_make(path, packages, keep)} {rel}")
cfg = top / CONFIGSTORE_MK
if not cfg.is_file():
print(f"FAIL: missing {cfg}", file=sys.stderr)
return 1
results.append(f"{ensure_disable_configstore(cfg)} {CONFIGSTORE_MK}")
pb = top / "build/make/target/product/base_system.mk"
if not pb.is_file():
print(f"FAIL: missing {pb}", file=sys.stderr)
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Guard GoogleParts EuiccDisabler before setApplicationEnabledSetting.
After wave1 drops com.google.android.euicc, Lineage GoogleParts still walks
EUICC_PACKAGES and calls setApplicationEnabledSetting unconditionally →
IllegalArgumentException: Unknown package: com.google.android.euicc.
Match EuiccPolicy's pattern: only touch packages that are installed.
Do not rename targets to OpenEUICC and do not disable OpenEUICC.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
REL = "device/google/sunfish/parts/src/org/lineageos/settings/EuiccDisabler.java"
OLD = """ public static void enableOrDisableEuicc(Context context) {
PackageManager pm = context.getPackageManager();
boolean disable = shouldDisable(pm);
int flag = disable
? PackageManager.COMPONENT_ENABLED_STATE_DISABLED
: PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
for (String pkg : EUICC_PACKAGES) {
pm.setApplicationEnabledSetting(pkg, flag, 0);
}
}
"""
NEW = """ private static boolean isInstalled(PackageManager pm, String pkgName) {
try {
pm.getPackageInfo(pkgName, 0);
return true;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
public static void enableOrDisableEuicc(Context context) {
PackageManager pm = context.getPackageManager();
boolean disable = shouldDisable(pm);
int flag = disable
? PackageManager.COMPONENT_ENABLED_STATE_DISABLED
: PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
for (String pkg : EUICC_PACKAGES) {
// Skip missing packages (e.g. EuiccGoogle after wave1 drop).
// Do not retarget to OpenEUICC; leave im.angry.openeuicc alone.
if (!isInstalled(pm, pkg)) {
continue;
}
pm.setApplicationEnabledSetting(pkg, flag, 0);
}
}
"""
def main() -> int:
top = Path(os.environ.get("ANDROID_BUILD_TOP", ".")).resolve()
path = top / REL
if not path.is_file():
print(f"FAIL: missing {path}", file=sys.stderr)
return 1
text = path.read_text()
if "Skip missing packages (e.g. EuiccGoogle after wave1 drop)" in text:
print(f"OK: already {REL}")
return 0
if OLD not in text:
print(f"FAIL: enableOrDisableEuicc block not found in {path}", file=sys.stderr)
return 1
path.write_text(text.replace(OLD, NEW, 1))
if "Skip missing packages (e.g. EuiccGoogle after wave1 drop)" not in path.read_text():
print(f"FAIL: patch did not land in {path}", file=sys.stderr)
return 1
print(f"OK: patched {REL}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Wire AXP device makefiles into LOS sunfish (product + board).
repo sync / forall reset drops uncommitted device/*.mk edits. Re-apply every
bacon:
* lineage_sunfish.mk → gms.mk + magisk.mk + apps.mk (Aurora, Droidify, …)
* BoardConfigLineage.mk → vendor/sunfish-a16/BoardConfig.mk (AVB, super size)
"""
from __future__ import annotations
import os
import pathlib
import sys
TREE = pathlib.Path(os.environ.get("SUNFISH_A16_TREE", "/srv/rom/src/lineage"))
PRODUCT_MK = TREE / "device/google/sunfish/lineage_sunfish.mk"
BOARD_MK = TREE / "device/google/sunfish/BoardConfigLineage.mk"
MARKER_PRODUCT = "sunfish-a16-009-product-includes"
MARKER_BOARD = "sunfish-a16-009-board-config"
PRODUCT_ANCHOR = "$(call inherit-product, vendor/google/sunfish/sunfish-vendor.mk)\n"
PRODUCT_BLOCK = (
"\n"
f"# {MARKER_PRODUCT}\n"
"$(call inherit-product-if-exists, vendor/sunfish-a16/gms.mk)\n"
"$(call inherit-product-if-exists, vendor/sunfish-a16/magisk.mk)\n"
"$(call inherit-product-if-exists, vendor/sunfish-a16/apps.mk)\n"
)
BOARD_ANCHOR = "include vendor/google/sunfish/BoardConfigVendor.mk\n"
BOARD_BLOCK = (
f"\n# {MARKER_BOARD}\n"
"-include vendor/sunfish-a16/BoardConfig.mk\n"
)
APPS_INHERIT = "$(call inherit-product-if-exists, vendor/sunfish-a16/apps.mk)\n"
MAGISK_INHERIT = "$(call inherit-product-if-exists, vendor/sunfish-a16/magisk.mk)\n"
def insert_once(path: pathlib.Path, anchor: str, block: str, marker: str, label: str) -> None:
text = path.read_text()
if marker in text:
print(f"OK: {label} already present")
return
if anchor not in text:
sys.exit(f"anchor missing in {path}")
path.write_text(text.replace(anchor, anchor + block, 1))
print(f"OK: {label} applied")
def ensure_apps_mk_in_product(path: pathlib.Path) -> None:
"""Marker can exist from an older block without apps.mk; repair without duplicating."""
text = path.read_text()
if MARKER_PRODUCT not in text or APPS_INHERIT.strip() in text:
return
if MAGISK_INHERIT not in text:
sys.exit(f"{path}: {MARKER_PRODUCT} present but magisk.mk inherit missing")
path.write_text(text.replace(MAGISK_INHERIT, MAGISK_INHERIT + APPS_INHERIT, 1))
print("OK: repaired product includes (added apps.mk)")
def main() -> None:
insert_once(PRODUCT_MK, PRODUCT_ANCHOR, PRODUCT_BLOCK, MARKER_PRODUCT, "product includes")
ensure_apps_mk_in_product(PRODUCT_MK)
insert_once(BOARD_MK, BOARD_ANCHOR, BOARD_BLOCK, MARKER_BOARD, "board config include")
if __name__ == "__main__":
main()
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Silence false pixel-thermal CDEV ERROR on sunfish split thermal stack.
sunfish ships CoolingDevices in thermal_info_config for Android reporting, but
does not BindedCdev/CdevRequest any of them — Qualcomm thermal-engine owns
CPU/GPU/FCC/modem mitigation. Newer pixel thermal HAL still calls
logCoolingDeviceStatus() for every CoolingDevice and getCdevMaxRequest()
ERROR-spams when the HAL vote map is empty (no sensor binds that cdev).
Only sunfish among device/google/*/thermal_info_config*.json has CoolingDevices
without BindedCdev. Skip unbound cdevs in the periodic CDEV log so both stacks
can run in parallel without ERROR noise. Real throttling paths still ERROR if a
binded cdev is missing from the vote map.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
REL = "hardware/google/pixel/thermal/utils/thermal_throttling.cpp"
OLD = """void ThermalThrottling::logCoolingDeviceStatus(
const std::unordered_map<std::string, CdevInfo> &cooling_device_info_map) {
int max_state = 0;
std::ostringstream cdev_log;
for (const auto &[cdev_name, cdev_info] : cooling_device_info_map) {
if (getCdevMaxRequest(cdev_name, &max_state)) {
ATRACE_INT((cdev_name + std::string("-state")).c_str(), max_state);
if (!cdev_info.apply_powercap) {
cdev_log << cdev_name << " state:" << max_state << " ";
} else {
const auto budget = static_cast<int>(
std::lround(cdev_info.state2power[max_state] / cdev_info.multiplier));
cdev_log << cdev_name << " state:" << max_state << ",budget:" << budget << " ";
ATRACE_INT((cdev_name + std::string("-budget")).c_str(), budget);
}
}
}
LOG(INFO) << "CDEV log " << cdev_log.str();
}
"""
NEW = """void ThermalThrottling::logCoolingDeviceStatus(
const std::unordered_map<std::string, CdevInfo> &cooling_device_info_map) {
int max_state = 0;
std::ostringstream cdev_log;
for (const auto &[cdev_name, cdev_info] : cooling_device_info_map) {
// Sunfish (and any device that lists CoolingDevices for Android
// reporting while an external mitigator votes): HAL vote map has no
// entry. Do not call getCdevMaxRequest — it would ERROR every ~5 min.
{
std::shared_lock<std::shared_mutex> _lock(cdev_all_request_map_mutex_);
if (!cdev_all_request_map_.count(cdev_name)) {
LOG(VERBOSE) << "Cooling device [" << cdev_name
<< "] has no HAL throttle votes (external mitigator)";
continue;
}
}
if (getCdevMaxRequest(cdev_name, &max_state)) {
ATRACE_INT((cdev_name + std::string("-state")).c_str(), max_state);
if (!cdev_info.apply_powercap) {
cdev_log << cdev_name << " state:" << max_state << " ";
} else {
const auto budget = static_cast<int>(
std::lround(cdev_info.state2power[max_state] / cdev_info.multiplier));
cdev_log << cdev_name << " state:" << max_state << ",budget:" << budget << " ";
ATRACE_INT((cdev_name + std::string("-budget")).c_str(), budget);
}
}
}
LOG(INFO) << "CDEV log " << cdev_log.str();
}
"""
MARKER = "has no HAL throttle votes (external mitigator)"
def main() -> int:
top = Path(os.environ.get("ANDROID_BUILD_TOP", ".")).resolve()
path = top / REL
if not path.is_file():
print(f"FAIL: missing {path}", file=sys.stderr)
return 1
text = path.read_text()
if MARKER in text:
print(f"OK: already {REL}")
return 0
if OLD not in text:
print(f"FAIL: logCoolingDeviceStatus block not found in {path}", file=sys.stderr)
return 1
path.write_text(text.replace(OLD, NEW, 1))
if MARKER not in path.read_text():
print(f"FAIL: patch did not land in {path}", file=sys.stderr)
return 1
print(f"OK: patched {REL}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -62,6 +62,6 @@ FossifyGallery present.
## Out of scope
- `CONFIG_SECURITY_YAMA` (kernel / US-S5 — **done** on `162840Z`)
- `CONFIG_SECURITY_YAMA` (kernel / US-S5 — unset like GOLDEN since 2026-10-10; scope 2 broke PairIP)
- Switching lunch to `user` (US-S6)
- NeoLauncher / Aosmium (wont / exception)
@@ -1,10 +1,12 @@
#!/bin/bash
# US-S5-1 — enable remaining Graphene/AXP-class defconfig knobs on msm-4.14.
# US-S5-1 — remaining Graphene/AXP-class defconfig knobs on msm-4.14.
#
# Oracle: AXP/Graphene kernel hardening expects Yama + slab freelist random
# (combo4 already ships SLAB_FREELIST_HARDENED). LOS sunfish_defconfig left both
# off; CONFIG_LSM already lists yama but SECURITY_YAMA was unset so the LSM
# never built.
# SLAB_FREELIST_RANDOM: LOS sunfish_defconfig leaves it off (combo4 already
# ships SLAB_FREELIST_HARDENED).
#
# SECURITY_YAMA: GOLDEN's running 4.14.355 config leaves it unset, so AXP's
# init ptrace_scope writes are inert there. With Yama built, scope 2 denies
# PairIP's self-ptrace (Wallapop SIGSEGV, LAB 2026-10-10). Match GOLDEN.
#
# Does NOT apply high-offset CVE leftovers (nf_tables deliberately skipped).
# CVE-2022-49980's one-liner stays on the denylist. The bacon applies
@@ -20,16 +22,15 @@ set -euo pipefail
K=${1:?kernel tree}
DEF="$K/arch/arm64/configs/sunfish_defconfig"
test -f "$DEF"
test -f "$K/security/yama/Kconfig"
for k in SECURITY_YAMA SLAB_FREELIST_RANDOM USERFAULTFD HAVE_ARCH_USERFAULTFD_MINOR; do
sed -i "/CONFIG_${k}/d" "$DEF"
done
{
echo "CONFIG_SECURITY_YAMA=y"
echo "# CONFIG_SECURITY_YAMA is not set"
echo "CONFIG_SLAB_FREELIST_RANDOM=y"
echo "# CONFIG_USERFAULTFD is not set"
} >> "$DEF"
echo "OK: US-S5-1 defconfig (YAMA + SLAB_FREELIST_RANDOM, USERFAULTFD off) on $K"
echo "OK: US-S5-1 defconfig (SLAB_FREELIST_RANDOM; YAMA, USERFAULTFD off) on $K"
@@ -1,4 +1,4 @@
# US-S5-1 — Yama + SLAB freelist random (defconfig)
# US-S5-1 — SLAB freelist random (defconfig); Yama off like GOLDEN
**Track**: AXP Resurection Pro Magisk, unlocked sunfish.
**On top of**: Stage1 + combo4 + FORTIFY + CVE clean-429 + hi-batch1–3.
@@ -12,9 +12,17 @@ quarantine:
| Knob | Before | After | Attribution |
|------|--------|-------|-------------|
| `CONFIG_SECURITY_YAMA` | unset (but `CONFIG_LSM` listed yama) | `=y` | Graphene/AXP LSM stack; ptrace scope |
| `CONFIG_SECURITY_YAMA` | unset (but `CONFIG_LSM` listed yama) | unset | GOLDEN 4.14.355 config: not set |
| `CONFIG_SLAB_FREELIST_RANDOM` | unset | `=y` | Graphene/AH slab hardening companion to existing `SLAB_FREELIST_HARDENED` |
Yama was `=y` from `162840Z` until 2026-10-10. With `apply_009_yama_ptrace.py`
init set `ptrace_scope=2`, which denies PairIP's self-ptrace. Wallapop then
died with SIGSEGV right after `libpairipcore.so` loaded. dmesg showed
`ptrace attach of "com.wallapop"[…] was attempted by "com.wallapop"[…]`.
With `persist.native_debug=true` (scope 0), LAB passed PairIP. GOLDEN has no
Yama, so AXP's init writes are inert there. The AXP init/sepolicy patch stays,
inert in the same way.
## Apply
```bash
@@ -28,17 +36,12 @@ Stage1+combo4+CVE apply.
## LAB verify
```text
zcat /proc/config.gz | grep CONFIG_SECURITY_YAMA=y
zcat /proc/config.gz | grep 'CONFIG_SECURITY_YAMA is not set'
zcat /proc/config.gz | grep CONFIG_SLAB_FREELIST_RANDOM=y
cat /proc/sys/kernel/yama/ptrace_scope # expect 2 (ship policy)
ls /proc/sys/kernel/yama # expect: no such file
pidof magiskd # still up
```
The ship script now applies `apply_009_yama_ptrace.py`, so the current LAB
policy is `ptrace_scope=2`, not the kernel default `1`. Matching AXP's
`persist.native_debug` 0/2 toggle is a separate userspace change and must not
be mixed into the CVE-2022-49980 boot test.
## Out of scope here
- The original `CVE-2022-49980/ANY/0001.patch` remains denylisted. The next
@@ -57,28 +57,42 @@ def rewrite_region_comment(bp: pathlib.Path) -> None:
print(f"region comment: {bp}")
def _git(dest: pathlib.Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
cmd = ["git", "-c", f"safe.directory={dest}", "-C", str(dest), *args]
if check:
return subprocess.run(cmd, check=True, text=True, capture_output=True)
return subprocess.run(cmd, text=True, capture_output=True)
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()
head = _git(dest, "rev-parse", "HEAD").stdout.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])
_git(dest, "fetch", "--depth", "1", "origin", PIN)
_git(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)]
[
"git",
"-c",
f"safe.directory={dest}",
"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])
_git(dest, "fetch", "--depth", "1", "origin", PIN)
_git(dest, "checkout", "--detach", PIN)
bp = dest / "Android.bp"
text = bp.read_text()
mte = """ device_has_arm_mte: {
@@ -106,9 +120,7 @@ def ensure_library() -> None:
1,
)
bp.write_text(text)
head = subprocess.check_output(
["git", "-C", dest, "rev-parse", "HEAD"], text=True
).strip()
head = _git(dest, "rev-parse", "HEAD").stdout.strip()
if head != PIN:
sys.exit(f"hardened_malloc HEAD {head} != {PIN}")
print(f"hardened_malloc {PIN}")