magisk-zapret2/tests/device
Repository files (latest commit first)
Filename Latest commit message Latest commit date
loop-uh 05b9e0af8c Let one preset change be one transaction, not six conversations
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>
2026-07-25 16:58:07 +03:00
..
README.md 2.0.8 2026-07-24 19:32:25 +03:00
smoke.sh Let one preset change be one transaction, not six conversations 2026-07-25 16:58:07 +03:00
test-smoke.sh Let one preset change be one transaction, not six conversations 2026-07-25 16:58:07 +03:00

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, and uninstall each 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

  • adb and a SHA-256 utility (sha256sum, shasum, or openssl) 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:

  1. --stage stop (runs and verifies stop twice)
  2. --stage start (runs and verifies start twice)
  3. --stage restart (runs and verifies restart twice)
  4. --stage update --allow-update plus the same four artifact arguments
  5. --stage full-rollback --allow-full-rollback
  6. --stage uninstall --allow-uninstall
  7. Remove only Zapret2 in the trusted root-manager UI, reboot, then run the read-only --stage uninstall-verify with 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 ordered version, pid, starttime, token, and module_dir fields whose recorded process identity is proven absent or has a readable, different starttime. A present but unreadable or malformed /proc/<pid>/stat is 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.