Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@
are absent), so shell regressions are caught locally before the push.

### Bug fixes
- **The Magisk denylist now hides root in the Play-Integrity/DroidGuard process (#170).** Denylist entries take a `package[/process]` shape, and `magisk-config.sh` inserts the real package into `package_name` and the process into `process` — never copying one value into both columns. The default enrolls `com.google.android.gms.unstable` (DroidGuard) as a **process** of the `com.google.android.gms` package instead of a bogus `package_name` that matched no installed app, so vanilla (non-Shamiko) Magisk actually hides root in the attestation process. The grammar validates both halves (SQL-injection prophylaxis), the health check matches enrolment by the package half, and the example configs demonstrate the corrected form.
- **`registry._read()` no longer silently drops an instance row that fails row-level validation (#252).** A row with a known backend kind but a rejected payload is now preserved opaquely so its port index stays reserved and it round-trips; a row too broken to salvage (bad `created_at`, missing/non-int index) is surfaced loudly (backed up to `.bak` with a hint) instead of being dropped and having its index silently reused.
- **`create`/`register` now refuse a directory that nests inside — or contains — another registered instance (#255).** Previously the overlap guard existed only for `restore`, so a nested `create` could later be wiped out by a `destroy` of the outer instance. The guard runs before any `mkdir`/registry write, so a refused operation is a no-op.
- **`beetroot adopt <IP:port>` (the help's own example) now auto-derives a valid instance name (#257).** `_adopt_default_name` collapses every non-alphanumeric run (including the dots in an IP) to a hyphen, so a network serial no longer fails the name-grammar guard and demands `--name`.
Expand Down
32 changes: 27 additions & 5 deletions docker/magisk-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,17 @@
# internally. Echoed in the waiting log so the user knows which DB
# we're targeting when v0.4 stealth-posture work randomises it.
# BEETROOT_DENYLIST_PACKAGES=
# Comma-separated list of Android package ids to enrol in Magisk's
# denylist (per-package SQL-injection prophylaxis lives in pydantic;
# see Magisk._check_packages in src/beetroot/config.py). Empty by
# default — the helper SQL'es nothing extra.
# Comma-separated list of ``package[/process]`` entries to enrol in
# Magisk's denylist. Each entry is a package, optionally followed by a
# slash and a process that belongs to it; no slash means the process
# equals the package. The helper splits on the first '/' and INSERTs the
# package into package_name and the process into process — Magisk keys the
# denylist on (package_name, process), so the DroidGuard process
# com.google.android.gms.unstable must be enrolled under its real package
# com.google.android.gms, never as a package of its own (issue #170).
# Per-half SQL-injection prophylaxis lives in pydantic; see
# Magisk._check_packages in src/beetroot/config.py. Empty by default —
# the helper SQL'es nothing extra.
# BEETROOT_MAGISK_WAIT_SECS=120
# Upper bound (in 1-second probe attempts) on the Magisk daemon wait.
# Conservative because a first boot of redroid+Magisk can legitimately
Expand Down Expand Up @@ -129,6 +136,21 @@ if [ -n "$DENYLIST_PACKAGES" ]; then
if [ -z "$pkg" ]; then
continue
fi
magisk --sqlite "INSERT OR IGNORE INTO denylist (package_name, process) VALUES ('$pkg', '$pkg');"
# Split the ``package[/process]`` entry (issue #170). The package is
# everything before the first '/', the process everything after it;
# an entry with no '/' has the process default to the package. Magisk
# keys the denylist on (package_name, process), so the DroidGuard
# process com.google.android.gms.unstable must land in the process
# column under its real package com.google.android.gms — never copied
# into package_name, where it matches no installed package and vanilla
# (non-Shamiko) Magisk never hides root. Toybox sh has no arrays; use
# POSIX parameter expansion (``%%/*`` = before first '/', ``#*/`` =
# after first '/') rather than a bash-ism.
pkg_name="${pkg%%/*}"
pkg_proc="${pkg#*/}"
if [ "$pkg_proc" = "$pkg" ]; then
pkg_proc="$pkg_name"
fi
magisk --sqlite "INSERT OR IGNORE INTO denylist (package_name, process) VALUES ('$pkg_name', '$pkg_proc');"
done
fi
20 changes: 16 additions & 4 deletions docs/guides/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,23 @@ android:
magisk:
denylist:
- com.google.android.gms
- com.google.android.gms.unstable
- com.google.android.gms/com.google.android.gms.unstable
```

Use this when you're testing something that doesn't perform anti-root checks, or when you want the lightest-weight setup with sensible defaults.

!!! note "Denylist entries are `package[/process]`"
Each `magisk.denylist` entry is a package, optionally followed by a slash and a **process** that belongs to it (`package/process`). No slash means the process is the package itself. This shape matters because Magisk keys the denylist on `(package_name, process)`: DroidGuard (Play Integrity) runs as the `com.google.android.gms.unstable` **process** of the `com.google.android.gms` package — it is not an installed package of its own. So the schema default enrols it as:

```yaml
magisk:
denylist:
- com.google.android.gms
- com.google.android.gms/com.google.android.gms.unstable # DroidGuard process
```

Enrolling the bare `com.google.android.gms.unstable` as if it were a package matches no installed app, and vanilla (non-Shamiko) Magisk then never hides root in DroidGuard. See the [`magisk` config reference](../reference/config.md#magisk).

### `stealth.yaml`

A wider Magisk denylist suitable for use with a root-hider like [Shamiko](https://github.com/LSPosed/LSPosed.github.io). Shamiko turns Magisk's denylist mode into a true allowlist-based hide — processes on the denylist can't detect Magisk at all. The denylist below covers all GMS variants and the Play Store.
Expand All @@ -54,8 +66,8 @@ android:
magisk:
denylist:
- com.google.android.gms
- com.google.android.gms.unstable
- com.google.android.gms.persistent
- com.google.android.gms/com.google.android.gms.unstable
- com.google.android.gms/com.google.android.gms.persistent
- com.android.vending
```

Expand Down Expand Up @@ -90,7 +102,7 @@ frida:
magisk:
denylist:
- com.google.android.gms
- com.google.android.gms.unstable
- com.google.android.gms/com.google.android.gms.unstable
```

Drop the `frida:` block (or copy `examples/default.yaml`) to turn Frida back off.
Expand Down
10 changes: 6 additions & 4 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,17 +292,19 @@ The variable-length port list is written to a per-instance `compose.override.yam

## `magisk`

Magisk configuration, including the boot-time denylist. Processes listed here are denylisted in the Magisk SQLite database at boot time, before any app launches.
Magisk configuration, including the boot-time denylist. Entries listed here are enrolled in the Magisk SQLite database at boot time, before any app launches.

Each entry is encoded as `package[/process]` — a package, optionally followed by a slash and a **process** that belongs to it. With no slash, the process is the package itself (root is hidden from the package's main process). With a slash, the package goes into the denylist's `package_name` column and the process into its `process` column. This matters because Magisk keys the denylist on `(package_name, process)`: Play Integrity's DroidGuard runs as the `com.google.android.gms.unstable` **process** of the `com.google.android.gms` package — it is *not* an installed package of its own, so it must be enrolled as `com.google.android.gms/com.google.android.gms.unstable`. Enrolling the bare `com.google.android.gms.unstable` as if it were a package matches no installed app, and vanilla (non-Shamiko) Magisk then never hides root there.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `denylist` | list[string] | `["com.google.android.gms", "com.google.android.gms.unstable"]` | Package names to add to Magisk's denylist. Each entry must match the Android package-id grammar (`[a-zA-Z0-9._]+`) — validated at load time as SQL-injection prophylaxis. |
| `denylist` | list[string] | `["com.google.android.gms", "com.google.android.gms/com.google.android.gms.unstable"]` | `package[/process]` entries to enrol in Magisk's denylist. Both halves must match the Android package-id grammar (`[a-zA-Z0-9._]+`), separated by at most one `/` — validated at load time as SQL-injection prophylaxis. The default hides root in the GMS main process **and** its `.unstable` DroidGuard process, both under the real `com.google.android.gms` package. |

```yaml
magisk:
denylist:
- com.google.android.gms
- com.google.android.gms.unstable
- com.google.android.gms/com.google.android.gms.unstable # DroidGuard process of GMS
- com.google.android.gms.persistent
- com.android.vending
```
Expand Down Expand Up @@ -440,7 +442,7 @@ modules:
magisk:
denylist:
- com.google.android.gms
- com.google.android.gms.unstable
- com.google.android.gms/com.google.android.gms.unstable # DroidGuard process of GMS
- com.google.android.gms.persistent
- com.android.vending
- com.target.app
Expand Down
2 changes: 1 addition & 1 deletion examples/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ android:
magisk:
denylist:
- com.google.android.gms
- com.google.android.gms.unstable
- com.google.android.gms/com.google.android.gms.unstable
2 changes: 1 addition & 1 deletion examples/lsposed.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ modules:
magisk:
denylist:
- com.google.android.gms
- com.google.android.gms.unstable
- com.google.android.gms/com.google.android.gms.unstable
4 changes: 2 additions & 2 deletions examples/stealth.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ android:
magisk:
denylist:
- com.google.android.gms
- com.google.android.gms.unstable
- com.google.android.gms.persistent
- com.google.android.gms/com.google.android.gms.unstable
- com.google.android.gms/com.google.android.gms.persistent
- com.android.vending
2 changes: 1 addition & 1 deletion examples/with-frida.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ frida:
magisk:
denylist:
- com.google.android.gms
- com.google.android.gms.unstable
- com.google.android.gms/com.google.android.gms.unstable
7 changes: 6 additions & 1 deletion src/beetroot/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1487,10 +1487,15 @@ def health(self) -> dict[str, CheckResult]:
checks["magisk.zygisk"] = _check_magisk_zygisk_over_adb(f"localhost:{adb_port}")
denylist = self._cfg.magisk.denylist
gms_pkg = "com.google.android.gms"
# Denylist entries are ``package[/process]`` (issue #170); match by the
# PACKAGE half so an entry that only writes ``gms/<proc>`` still counts
# as enrolled (the SQL check keys on package_name, which is now the real
# package regardless of the process the entry targets).
enrolled = any(entry.split("/", 1)[0] == gms_pkg for entry in denylist)
checks[f"magisk.denylist.{gms_pkg}"] = _check_magisk_denylist_over_adb(
f"localhost:{adb_port}",
gms_pkg,
enrolled=gms_pkg in denylist,
enrolled=enrolled,
)
return checks

Expand Down
67 changes: 43 additions & 24 deletions src/beetroot/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,17 @@ def validate_android_version(v: int) -> int:
_MIN_PORT: Final = 1
_MAX_PORT: Final = 65535

# Magisk/stealth denylist packages must look like a normal Android package
# id: alphanumerics, dots, and underscores only. Pre-validated at
# config-load time as SQL-injection prophylaxis for the wire-up of
# the denylist through ``magisk-config.sh``'s sqlite REPLACE INTO.
_DENYLIST_PKG_RE: Final = re.compile(r"^[a-zA-Z0-9._]+$")
# Magisk/stealth denylist entries encode a package and an optional process it
# belongs to as ``package[/process]``. Both halves must look like a normal
# Android package id: alphanumerics, dots, and underscores only (the ``/`` is
# only the separator, never inside a half). Magisk's denylist keys on
# (package_name, process), and a process like ``com.google.android.gms.unstable``
# (DroidGuard) is NOT an installed package — it must be enrolled under its real
# package ``com.google.android.gms`` or vanilla (non-Shamiko) Magisk never hides
# root there (issue #170). Pre-validated at config-load time — on BOTH halves —
# as SQL-injection prophylaxis for the wire-up of the denylist through
# ``magisk-config.sh``'s sqlite INSERT.
_DENYLIST_PKG_RE: Final = re.compile(r"^[a-zA-Z0-9._]+(/[a-zA-Z0-9._]+)?$")

# Frida release tags follow the major.minor.patch shape upstream.
# Pre-validated so a typo in ``frida.version`` (e.g. ``"16.4"`` or
Expand Down Expand Up @@ -474,7 +480,7 @@ def model_post_init(self, _ctx: object) -> None:

_DEFAULT_DENYLIST: Final = (
"com.google.android.gms",
"com.google.android.gms.unstable",
"com.google.android.gms/com.google.android.gms.unstable",
)


Expand All @@ -483,29 +489,40 @@ class Magisk(BaseModel):
Magisk configuration, including the boot-time denylist.

Attributes:
denylist: Package names added to Magisk's denylist at boot. Each
entry must match the Android package-id grammar
(``[a-zA-Z0-9._]+``) — see :data:`_DENYLIST_PKG_RE`. The
grammar is enforced at validation time so
``magisk-config.sh`` can compose the entries into a SQLite
REPLACE-INTO statement without escaping; any shape that
wouldn't be a valid package name today is assumed to be
either a typo or an injection attempt.
Defaults to the GMS package pair (the v0.3 helper enrolled
these unconditionally; the config move keeps the default
behaviour identical while putting the user in control).
denylist: Entries added to Magisk's denylist at boot, each encoded
as ``package[/process]`` — a package, optionally followed by a
slash and a process that belongs to it (no slash means the
process equals the package). Both halves must match the Android
package-id grammar (``[a-zA-Z0-9._]+``) — see
:data:`_DENYLIST_PKG_RE`. The grammar is enforced on BOTH halves
at validation time so ``magisk-config.sh`` can split the entry
and compose the package + process into a SQLite INSERT without
escaping; any shape that wouldn't be a valid package name today
is assumed to be either a typo or an injection attempt. The
``package/process`` form matters because Magisk's denylist keys
on ``(package_name, process)``: DroidGuard runs as the
``com.google.android.gms.unstable`` *process* of the
``com.google.android.gms`` package — not an installed package of
its own — so it must be enrolled under its real package or
vanilla (non-Shamiko) Magisk never hides root there (issue #170).
Defaults to the GMS main process plus the ``.unstable``
DroidGuard process, both under the real ``com.google.android.gms``
package (the v0.3 helper enrolled the GMS pair unconditionally;
the config move keeps the intent identical while putting the user
in control and fixing the process it targets).
"""

denylist: list[str] = Field(default_factory=lambda: list(_DEFAULT_DENYLIST))

@field_validator("denylist")
@classmethod
def _check_packages(cls, value: list[str]) -> list[str]:
for pkg in value:
if not _DENYLIST_PKG_RE.match(pkg):
for entry in value:
if not _DENYLIST_PKG_RE.match(entry):
raise ValueError(
f"magisk.denylist entry {pkg!r} is not a valid Android "
"package id (must match [a-zA-Z0-9._]+)"
f"magisk.denylist entry {entry!r} is not a valid "
"package[/process] id (both halves must match [a-zA-Z0-9._]+, "
"with an optional single '/' separating package and process)"
)
return value

Expand Down Expand Up @@ -1313,9 +1330,11 @@ def render_env(
f"DISPLAY_FPS={cfg.display.fps}",
f"DISPLAY_GPU={resolve_rendering(cfg.display.rendering)}",
# Encoded as a comma-separated list because toybox sh has no array
# support — the helper iterates over ``IFS=,``. Per-package shape is
# already validated by the ``Magisk._check_packages`` regex, so we
# can safely join with a delimiter that's not in the package-id grammar.
# support — the helper iterates over ``IFS=,``, then splits each
# ``package[/process]`` entry on the ``/``. Per-entry shape (both
# halves + the single optional ``/``) is already validated by the
# ``Magisk._check_packages`` regex, so we can safely join with a comma
# (which is not in the entry grammar) and split on ``/`` in the helper.
f"BEETROOT_DENYLIST_PACKAGES={','.join(cfg.magisk.denylist)}",
# v0.4 stealth-posture overrides — emitted with the known-safe
# defaults. render_env is the single source of truth instead of the
Expand Down
Loading