fix(build): bound the prerm hooks build.sh writes into the packages

build.sh assembles the APK and IPK packages itself rather than going through
OpenWrt's packaging, so it carries its own copies of the prerm-family hooks.
Bounding Package/tachyon/prerm in tachyon/Makefile therefore never reached the
released artifacts: 1.2.66 shipped with

    exit(system("/usr/bin/tachyon package_prerm upgrade >/dev/null 2>&1"));

which is both unbounded and status-propagating. `exit(system(...))` returns a raw
wait status, so a cleanup that was merely killed took the upgrade down with it —
on the router that surfaced as

    ERROR: lib/apk/exec/tachyon-1.2.66.pre-upgrade: exited with error 247

247 being 128+119, the cleanup SIGKILLed by its own sweep.

All three hooks written here (APK pre-upgrade, APK pre-deinstall, backend IPK
prerm) now probe the BusyBox timeout spelling against /bin/true, bound the
cleanup at 90s, and exit 0 unconditionally — matching the Makefile.

tests/package_lifecycle.sh gains contracts over all three: each must be bounded,
must not propagate the cleanup's status, and must compile. They live inside
heredocs, so ucode_syntax_lint.sh never sees them and a syntax error would only
surface on a router mid-upgrade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dushnilin
2026-08-09 21:13:33 +03:00
co-authored by Claude Opus 5
parent 4ce5d7ebac
commit 84924a0fff
2 changed files with 85 additions and 6 deletions
+47 -6
View File
@@ -387,8 +387,17 @@ EOF
cat > "$control_dir/prerm" <<'EOF'
#!/usr/bin/ucode
if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "")
system("/usr/bin/tachyon package_prerm >/dev/null 2>&1");
// Bounded from the outside like every other prerm here: the cleanup being invoked
// is the OLD installed one, so it cannot be trusted to bound itself.
if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "") {
let prefix = "";
if (system("timeout 1 /bin/true >/dev/null 2>&1") == 0)
prefix = "timeout 90 ";
else if (system("timeout -t 1 /bin/true >/dev/null 2>&1") == 0)
prefix = "timeout -t 90 ";
system(prefix + "/usr/bin/tachyon package_prerm >/dev/null 2>&1");
}
exit(0);
EOF
@@ -596,16 +605,48 @@ EOF
cat > "$scripts_dir/backend-pre-deinstall.sh" <<'EOF'
#!/usr/bin/ucode
if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "")
system("/usr/bin/tachyon package_prerm >/dev/null 2>&1");
// Bounded for the same reason as backend-pre-upgrade.sh: this runs the OLD
// installed cleanup, and a hook that never returns takes the removal down with it.
if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "") {
let prefix = "";
if (system("timeout 1 /bin/true >/dev/null 2>&1") == 0)
prefix = "timeout 90 ";
else if (system("timeout -t 1 /bin/true >/dev/null 2>&1") == 0)
prefix = "timeout -t 90 ";
system(prefix + "/usr/bin/tachyon package_prerm >/dev/null 2>&1");
}
exit(0);
EOF
cat > "$scripts_dir/backend-pre-upgrade.sh" <<'EOF'
#!/usr/bin/ucode
if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "")
exit(system("/usr/bin/tachyon package_prerm upgrade >/dev/null 2>&1"));
// Must mirror Package/tachyon/prerm in tachyon/Makefile: the APK packages are
// assembled here rather than by OpenWrt's packaging, so a fix applied only to the
// Makefile never reaches them.
//
// apk rolls the whole transaction back if this hook fails or outlives its
// patience, which leaves the old build installed. The cleanup invoked below
// belongs to the OLD version, so it cannot be trusted to bound itself — the hook
// bounds it from the outside. BusyBox timeout wanted `-t SECONDS` on older builds
// and rejects it on newer ones; the spelling is probed against /bin/true because
// probing with the real command would run the cleanup twice whenever the first
// form is unsupported.
if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "") {
let prefix = "";
if (system("timeout 1 /bin/true >/dev/null 2>&1") == 0)
prefix = "timeout 90 ";
else if (system("timeout -t 1 /bin/true >/dev/null 2>&1") == 0)
prefix = "timeout -t 90 ";
system(prefix + "/usr/bin/tachyon package_prerm upgrade >/dev/null 2>&1");
}
// Whatever the cleanup managed to do, the upgrade itself must proceed: the old
// `exit(system(...))` form returned a raw wait status, so a killed or merely
// unhappy cleanup aborted the upgrade and rolled the new build back.
exit(0);
EOF