-
Notifications
You must be signed in to change notification settings - Fork 0
fix: prove installed module bytes during updates #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d100809
ac90ddd
efc8740
42d1f3a
de41577
bbdb197
4727761
d1061e4
f75fed9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -173,6 +173,86 @@ push() { | |
| "$ADB_BIN" -s "$SERIAL" push "$1" "$2" >/dev/null | ||
| } | ||
|
|
||
| # The pinned Docker Engine identity is read only from the engine.tarball | ||
| # object; the binaries block carries its own size and sha256 keys and must | ||
| # never satisfy these parses. | ||
| engine_tarball_block() { | ||
| sed -n '/"tarball": {/,/}/p' "$1" | ||
| } | ||
|
|
||
| engine_archive_name() { | ||
| local engine_json=$SCRIPT_DIR/engine.json url | ||
| [[ -f "$engine_json" ]] || engine_json=$SCRIPT_DIR/../tools/engine.json | ||
| url=$(engine_tarball_block "$engine_json" 2>/dev/null | sed -n 's/.*"url": "\([^"]*\)".*/\1/p') | ||
| printf '%s' "${url##*/}" | ||
| [[ -n "$url" ]] | ||
| } | ||
|
Comment on lines
+183
to
+189
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If
Compare with Reachability today is narrow — |
||
|
|
||
| ensure_engine_archive() { | ||
| local engine_json engine_url engine_name engine_size engine_sha archive held held_size held_sha | ||
| engine_json=$SCRIPT_DIR/engine.json | ||
| [[ -f "$engine_json" ]] || engine_json=$SCRIPT_DIR/../tools/engine.json | ||
| engine_url= | ||
| engine_name= | ||
| engine_size= | ||
| engine_sha= | ||
| if [[ -f "$engine_json" ]]; then | ||
| engine_url=$(engine_tarball_block "$engine_json" | sed -n 's/.*"url": "\([^"]*\)".*/\1/p') | ||
| engine_name=${engine_url##*/} | ||
| engine_size=$(engine_tarball_block "$engine_json" | sed -n 's/.*"size": \([0-9][0-9]*\).*/\1/p') | ||
| engine_sha=$(engine_tarball_block "$engine_json" | sed -n 's/.*"sha256": "\([0-9a-f]\{64\}\)".*/\1/p') | ||
| fi | ||
| [[ -n "$engine_url" && -n "$engine_size" && -n "$engine_sha" ]] \ | ||
| || die 'cannot read the pinned Docker Engine identity' | ||
| if phone "test -s /data/local/tmp/$engine_name" >/dev/null 2>&1; then | ||
| held=$(phone "wc -c < /data/local/tmp/$engine_name; sha256sum /data/local/tmp/$engine_name" 2>/dev/null | tr -d '\r') || held= | ||
| held_size=$(printf '%s\n' "$held" | sed -n '1s/^[[:space:]]*\([0-9][0-9]*\).*/\1/p') | ||
| held_sha=$(printf '%s\n' "$held" | sed -n '2s/^\([0-9a-f]\{64\}\) .*/\1/p') | ||
| if [[ "$held_size" == "$engine_size" && "$held_sha" == "$engine_sha" ]]; then | ||
| return 0 | ||
| fi | ||
| printf 'install: the phone-held %s fails the pinned identity; refetching\n' "$engine_name" >&2 | ||
| fi | ||
| archive=$(mktemp "${TMPDIR:-/tmp}/eip-engine.XXXXXX") | ||
| curl --fail --location --proto '=https' --proto-redir '=https' \ | ||
| --output "$archive" "$engine_url" \ | ||
| || die 'the pinned Docker Engine archive could not be downloaded' | ||
| [[ $(wc -c < "$archive" | tr -d ' ') == "$engine_size" ]] \ | ||
| || die 'the pinned Docker Engine archive has the wrong size' | ||
| verify_file "$archive" "$engine_sha" 'Docker Engine archive' | ||
| push "$archive" "/data/local/tmp/$engine_name" | ||
| rm -f "$archive" | ||
| } | ||
|
|
||
| # KernelSU's module update can preserve previously installed file bytes | ||
| # (observed on the Pixel 11 Pro XL on 2026-09-15: a new module.prop beside a | ||
| # stale bin/hostctl), so installed module bytes are always re-staged from the | ||
| # payload through a mode-preserving overlay and proven on the phone against a | ||
| # payload-built checksum manifest, with files the payload no longer contains | ||
| # pruned. The overlay touches only the KernelSU module tree; the daemon | ||
| # runs from the separately managed /data/docker release tree. | ||
| verify_module_files() { | ||
| local work entry | ||
| stage 'Verifying the Pixel module bytes' 'Check the module verification output above; only the module tree was being restored, and the Docker daemon runs from the separate /data/docker release tree.' | ||
| work=$(mktemp -d "${TMPDIR:-/tmp}/eip-module.XXXXXX") | ||
| unzip -q "$PAYLOAD/host-module.zip" -d "$work/module" \ | ||
| || die 'the payload module archive cannot be extracted' | ||
| while IFS= read -r entry; do | ||
| [[ -n "$entry" ]] || continue | ||
| hash_file "$work/module/$entry" | ||
| printf '%s %s\n' "$FILE_SHA256" "$entry" | ||
| done < <(cd "$work/module" && LC_ALL=C find . -type f | LC_ALL=C sed 's|^\./||' | LC_ALL=C sort) \ | ||
| > "$work/manifest" | ||
| LC_ALL=C awk '{ $1 = ""; sub(/^ /, ""); print }' "$work/manifest" > "$work/names" | ||
| tar -C "$work/module" -cf "$work/files.tar" . | ||
| push "$work/files.tar" /data/local/tmp/eip-module-files.tar | ||
| push "$work/manifest" /data/local/tmp/eip-module-manifest | ||
| push "$work/names" /data/local/tmp/eip-module-names | ||
| phone 'tar -xf /data/local/tmp/eip-module-files.tar -C /data/adb/modules/eip-pixel8a-forge && chown -R 0:0 /data/adb/modules/eip-pixel8a-forge && cd /data/adb/modules/eip-pixel8a-forge && /data/adb/ksu/bin/busybox sha256sum -c /data/local/tmp/eip-module-manifest -s && find . -type f | sed "s|^\\./||" | LC_ALL=C sort | LC_ALL=C comm -23 - /data/local/tmp/eip-module-names | while IFS= read -r stale; do case "$stale" in disable|remove|update|skip_mount) continue ;; esac; rm -f "$stale"; done; n_names=$(wc -l < /data/local/tmp/eip-module-names); n_files=$(find . -type f | wc -l); n_markers=0; for m in disable remove update skip_mount; do [ -f "$m" ] && n_markers=$((n_markers + 1)); done; if [ "$n_files" -ne $((n_names + n_markers)) ]; then printf 'module tree contains unexpected files beyond the payload and module-state markers\n' >&2; exit 5; fi; rm -f /data/local/tmp/eip-module-files.tar /data/local/tmp/eip-module-manifest /data/local/tmp/eip-module-names; exit 0' \ | ||
| || die 'installed module files do not match the payload' | ||
| rm -rf "$work" | ||
| } | ||
|
|
||
| # adb install has been observed to stall indefinitely when the installer runs | ||
| # without a terminal, so every APK install is bounded and retried once. | ||
| APK_INSTALL_TIMEOUT_SECONDS=180 | ||
|
|
@@ -440,7 +520,10 @@ if [[ "$HOST_MODULE_CURRENT" == false ]]; then | |
| fi | ||
| stage 'Installing the Pixel Docker host' 'Check the module output above, package inputs, USB connection, and available phone storage.' | ||
| if [[ -f "$PAYLOAD/docker-engine.tgz" ]]; then | ||
| push "$PAYLOAD/docker-engine.tgz" /data/local/tmp/docker-29.8.0.tgz | ||
| bundled_engine_name=$(engine_archive_name) || die 'cannot read the pinned Docker Engine identity' | ||
| push "$PAYLOAD/docker-engine.tgz" "/data/local/tmp/$bundled_engine_name" | ||
| else | ||
| ensure_engine_archive | ||
| fi | ||
| push "$PAYLOAD/kernel.lz4" /data/local/tmp/Image-CP2A.260805.005.lz4 | ||
| push "$PAYLOAD/host-module.zip" /data/local/tmp/eip-pixel8a-forge.zip | ||
|
|
@@ -454,11 +537,16 @@ if [[ "$HOST_MODULE_CURRENT" == false ]]; then | |
| case "$slot" in _a|_b) ;; *) die "cannot determine active slot: $slot" ;; esac | ||
| module_root=$(phone 'if test -x /data/adb/modules_update/eip-pixel8a-forge/bin/kernelctl; then printf /data/adb/modules_update/eip-pixel8a-forge; else printf /data/adb/modules/eip-pixel8a-forge; fi' | tr -d '\r') | ||
| phone "KSU=true KSU_VER=3.3.0 KSU_VER_CODE=33214 KSU_RUNTIME_MODE=lkm $module_root/bin/kernelctl install INSTALL:CP2A.260805.005:$slot" | ||
| phone 'rm -f /data/local/tmp/docker-29.8.0.tgz /data/local/tmp/Image-CP2A.260805.005.lz4 /data/local/tmp/eip-pixel8a-forge.zip' | ||
| engine_cleanup_name=$(engine_archive_name) || die 'cannot read the pinned Docker Engine identity' | ||
| phone "rm -f /data/local/tmp/$engine_cleanup_name /data/local/tmp/Image-CP2A.260805.005.lz4 /data/local/tmp/eip-pixel8a-forge.zip" | ||
| "$ADB_BIN" -s "$SERIAL" reboot >/dev/null 2>&1 || true | ||
| wait_android | ||
| verify_module_files | ||
| elif [[ "$EXISTING_INSTALL" == false ]]; then | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This Contrast with the sibling branches that do verify:
This isn't just a theoretical gap — |
||
| stage 'Preparing Docker storage' 'Check the host storage error above and select the existing disk size if this is a partial installation.' | ||
| if [[ -f "$PAYLOAD/host-module.zip" ]]; then | ||
| verify_module_files | ||
| fi | ||
| phone "/data/docker/bin/hostctl disk-init --size-bytes $DISK_BYTES" | ||
| fi | ||
|
|
||
|
|
@@ -480,6 +568,9 @@ if [[ "$EXISTING_INSTALL" == true ]]; then | |
| install_control_app | ||
| stage 'Waiting for current Forge work to finish' 'Forge remains available until its active work is idle; close new work and wait.' | ||
| wait_until_parked | ||
| if [[ -f "$PAYLOAD/host-module.zip" ]]; then | ||
| verify_module_files | ||
| fi | ||
| stage 'Restarting Docker for the update' 'Forge remains parked; check the Docker startup error above.' | ||
| start_docker | ||
| stage 'Installing the matched Forge update' 'Check the source transaction error above; existing state is retained for rollback.' | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new
dieguard here can't actually stop the installer — it only kills a subshell.engine_archive_name()is only ever invoked via command substitution:$(...)always forks a subshell to capture stdout. Whennameis empty (missing/malformedengine.json, or the fallback$SCRIPT_DIR/../tools/engine.jsonalso absent),die 'cannot read the pinned Docker Engine identity'callsexit 1— but that only terminates the subshell. The parent script never sees the failure: the exit status of a command substitution embedded mid-word is discarded (it's not a bare assignment, soset -edoesn't fire on it either).The visible effect: the diagnostic prints to stderr, but the script keeps going with an empty substitution, so:
push "$PAYLOAD/docker-engine.tgz" "/data/local/tmp/"— adb pushes into that directory under the source basename (docker-engine.tgz), not the pinned namekernelctl/prepare-engineon the module side expect.rm -f /data/local/tmp/ ...— a silent no-op on the directory (-fswallows the "is a directory" error).So the exact safety check this PR adds ("guard the pin name") is dead code in the one case it exists to catch, and a broken bundle fails later with a confusing device-side error instead of the clean
diemessage here.Contrast with
hash_file/verify_fileelsewhere in this same script, which are always called as plain statements (never inside$(...)) specifically so their sets/exits reach the caller —engine_archive_namebreaks that existing convention.Suggested fix: capture into a variable and check explicitly at each call site, e.g.
name=$(engine_archive_name) || die '...'; push ... "/data/local/tmp/$name", rather than interpolating the call directly into a larger string.