| Filename | Latest commit message | Latest commit date |
|---|---|---|
Applying a preset was a conversation the app held with the module: snapshot runtime.ini, ask whether the service was running, read runtime.ini again, stage a candidate, commit it, verify it, then restart. Seven privileged round trips, each paying a libsu round trip and a full common.sh sourcing, and — worse — the rollback decisions lived in Kotlin. The app was deciding what the module's state was, which is exactly what the typed machine payload exists to prevent. zapret2/scripts/zapret-apply-preset.sh is now the single app-facing entry point for that mutation. It sources the lifecycle helpers once, inherits the Android lease through ZAPRET2_LIFECYCLE_TOKEN, and under that one lock it validates the request with the packaged name policy, qualifies the preset through the same compile-and-dry-run the preview machinery already owned, publishes the selection through runtime-config.sh --commit-candidate, and hands the replacement to zapret-start.sh --replace exactly as zapret-restart.sh does. Nothing here reimplements a transaction that already had an owner. Because the qualification happens before the commit, an incompatible preset is now refused with the live selection untouched instead of being written, failed and rolled back. A failed replacement restores the previous generation and says so, and a rollback that cannot restore it says that instead — the payload distinguishes the two, carrying the exact Z2_ERROR identity of whichever nested stage refused. A service the user had stopped is still only saved to, measured from the committed lifecycle receipt the way zapret-status.sh derives Z2_PROCESS. The app projects that payload and nothing more. It keeps the stepwise flow for one reason: a module generation installed before this entry point existed cannot grow it, so the same round trip answers with an unsupported sentinel and the old path runs. A payload that is truncated, inconsistent or written by a newer module is never guessed at — the repository reads the published selection, and accepts an application only when the live owner generation is the one this lease stamped, which is the same proof the restart path already required. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
| .. | ||
| README.md | ||
| smoke.sh | ||
| test-smoke.sh | ||
Rooted-device smoke harness
smoke.sh is an evidence-oriented release check for a disposable, recoverable
Android device. It is not a general installer. Its default and --dry-run
behaviour is strictly read-only on the device.
Safety contract
- Every invocation requires an exact ADB serial. The harness rejects missing, unauthorized, offline, ambiguous, or differently reported devices.
- A mutating stage is rejected before the first ADB call unless it has
--allow-mutations, an exact serial acknowledgement, and the literal tested recovery-plan acknowledgement. update,full-rollback, anduninstalleach have an additional opt-in.- The initial preflight creates a private (0700) evidence directory containing a before-state bundle. Firewall dumps and device metadata may be sensitive; retain the directory as release evidence and do not publish it unreviewed.
- Update uses the public Magisk module-install CLI and Android package manager.
ADB uploads to randomized shell-temporary names, then root verifies the
source inode/owner/mode/link count and copies both artifacts into a new
randomized root-owned 0700 directory. The root-private 0600 copies are
revalidated and hashed immediately before Magisk consumes the ZIP and before
the APK is streamed into
pm; cleanup removes only that exact, revalidated owned directory. Host, private-stage, installed-generation, and installed-APK SHA-256 identities must all match. The internal update guard is never invoked. - Uninstall is intentionally manual through the trusted root-manager UI. The harness records the last checkpoint with the canonical boot ID bound to the exact serial/evidence run and the private raw query's name, size, SHA-256, and canonical value, requires a different canonical boot ID during verification, and validates the exact secure five-field uninstall tombstone. It never uses Magisk's all-modules removal command or writes a removal marker.
Prerequisites
adband a SHA-256 utility (sha256sum,shasum, oropenssl) on the host.- A disposable ARM/ARM64 device on API 24+ with an unlocked, authorized ADB
session, Magisk 20.4+, working
su, and a tested recovery route. - Zapret2 already installed from the exact release under test.
- For update testing, both final signed CI artifacts and their independently retained SHA-256 values. Do not use a locally rebuilt substitute.
Preflight validates root, API/ABI, exact module/state paths and modes, packaged
entry points, iptables/ip6tables, NFQUEUE and --queue-bypass, optional
connbytes, mandatory multiport and mark, the 21-field status contract,
and artifact hashes. Missing connbytes selects upstream's outgoing
KEEPALIVE topology. IPv4 is mandatory. IPv6 is recorded and may be skipped
like runtime startup; use --require-ipv6 for an IPv6 matrix device.
The evidence bundle records exactly schema=1, ipv4=ready, and either
ipv6=ready or ipv6=not_available, in that order. Every device mutation
revalidates this record against the immediately preceding preflight. IPv4
always requires a successful iptables-save; recorded-ready IPv6 requires a
successful ip6tables-save, while unavailable IPv6 uses the canonical
Z2_IP6TABLES_SAVE=not_available evidence record.
Because preflight must not insert a rule, its capability result is based on the
installed firewall tools/extensions, their --queue-bypass interface, procfs
facts, and the status contract. The later start stage is what proves that the
kernel accepts and cleans up exact probe rules under the module's durable
transaction journal.
All privileged evidence queries carry an exact return-code/completion footer;
failed or truncated output is rejected rather than retained as successful
evidence. Rollback and uninstall additionally require successful IPv4/IPv6
firewall dumps with no owned chain, anchor, or rule, plus a /proc audit of
exe and cmdline bound to the exact owner metadata/PID files. A ps text
grep is not accepted as cleanup evidence. Status, rollback, root-stage, and
private-copy machine records reject missing, duplicate, reordered, and unknown
fields; root-stage creation must explicitly report Z2_ROOT_STAGE_SECURE=1.
Ordered run
Start with read-only preflight. Supply artifacts here if this evidence sequence will include an update, because later update must match the hashes bound here:
sh tests/device/smoke.sh \
--serial EXACT_ADB_SERIAL \
--evidence-dir /secure/evidence/zapret2-device-1 \
--module-zip /artifacts/zapret2-magisk-vX.Y.Z.zip \
--module-sha256 MODULE_SHA256 \
--apk /artifacts/zapret2-control-vX.Y.Z.apk \
--apk-sha256 APK_SHA256 \
--require-ipv6
For every mutating invocation, repeat these three gates:
--allow-mutations \
--ack-disposable-device EXACT_ADB_SERIAL \
--ack-recovery I_HAVE_A_TESTED_RECOVERY_PLAN
Use the same evidence directory and run one stage per invocation. The harness refuses skipped, reordered, repeated, or post-uninstall mutation stages:
--stage stop(runs and verifies stop twice)--stage start(runs and verifies start twice)--stage restart(runs and verifies restart twice)--stage update --allow-updateplus the same four artifact arguments--stage full-rollback --allow-full-rollback--stage uninstall --allow-uninstall- Remove only Zapret2 in the trusted root-manager UI, reboot, then run the
read-only
--stage uninstall-verifywith the same serial/evidence directory. Verification fails unless the boot ID changed and the remaining tombstone is a root-owned 0600 single-link file with orderedversion,pid,starttime,token, andmodule_dirfields whose recorded process identity is proven absent or has a readable, different starttime. A present but unreadable or malformed/proc/<pid>/statis unsafe and fails verification.
Before any mutating stage, append --dry-run to repeat all read-only checks and
show that the stage is eligible without executing it or advancing the sequence.
Full rollback's exact 10-field contract requires a reboot; the manual uninstall
checkpoint deliberately remains last. Reinstall verification is a separate
case: after uninstall verification, reinstall through the root manager and
start a new evidence directory from preflight.
Host safety tests
The host test uses a fake ADB executable and cannot contact a device:
sh tests/device/test-smoke.sh
It uses a deny-by-default fake ADB/root/package-manager allowlist. Besides
proving that default/preflight and dry-run execute no mutation, it runs the
ordered update, full rollback, manual-uninstall verification, and rejects
unknown commands or modes, every modeled staging/cleanup ownership fault, both
install-time hash swaps, capability drift, mandatory dump failures, dirty IPv4
or IPv6 state, failed/truncated queries, malformed exact machine schemas, and
false-clean process audits. It also rejects missing/malformed/same-boot evidence,
tampered run binding, and malformed, live, foreign, symlinked, or wrong-mode
uninstall tombstones and raw proc states. The fake executes the production root
audit over modeled filesystem/proc primitives; temporary-mutant kill checks
cover the reboot binding and root stat/schema/proc predicates. A separate
ordered IPv4-only run reaches
uninstall-verify using canonical IPv6 non-availability evidence throughout.