Skip to content

sucompat: uniform EACCES guard for /data/adb paths + dynamic guard prefix registration - #331

Open
AYwlilwYA wants to merge 1 commit into
bmax121:mainfrom
AYwlilwYA:pr-oracle-guard
Open

AYwlilwYA wants to merge 1 commit into
bmax121:mainfrom
AYwlilwYA:pr-oracle-guard

Conversation

@AYwlilwYA

Copy link
Copy Markdown

Problem

Unprivileged probes of /data/adb/<subdir> can distinguish existence via errno:

  • Stock behavior: /data/adb is 0700 root; DAC denies search before child
    lookup, so every path under the prefix returns EACCES uniformly. EACCES
    itself proves the directory exists, but which child exists is unobservable.
  • Path-hiding layers (module managers, hide modules) commonly answer hidden
    paths with ENOENT instead. The errno difference becomes an existence
    oracle
    that fingerprints the hiding stack — a detection vector KernelPatch
    users should not carry.

Measured on stock (Xiaomi nabu, Android 13): stat /data/adb and every tested
child → uniform EACCES. Measured on a hidden stack: hidden children → ENOENT.

Solution

1. Name-first guard at the getname wrap

security_file_open-class hooks fire after lookup; only the getname wrap
can unify the answer before the filesystem ever sees the path.

  • su_is_adb_prefix() — exact match of the /data/adb body and its subpaths
    (no false positive on /data/adbfoo).
  • after_getname_flags() — string-compare first; consult authorization only
    on a name hit
    . Hot path for non-target paths is a pure memcmp, with no
    per-call kstorage lookups (also closes the timing side channel that a
    lookup-first ordering would keep open). Unauthorized hit → putname +
    ERR_PTR(-EACCES) before lookup. Logging is rate-limited (first 4 hits).
  • Wired once via su_register_path_probe_hooks() (su_getname_hooked
    guard); also assigns su_dentry_path_raw (previously declared, never
    wired since 2f8f2af).

2. getattr insurance layer

after_security_inode_getattr() returns -EACCES for unauthorized callers
on guarded prefixes. It is implemented but not globally registered by
default
— available as a second layer for OEM kernels where the getname
wrap does not fire.

3. Dynamic guard prefix registration (optional API for KPMs / apps)

Kernels shipped to third-party modules often need to guard paths beyond the
hardcoded set:

  • su_guard_add_prefix(const char *prefix) / su_guard_del_prefix(const char *prefix)
    — exported via KP_EXPORT_SYMBOL (registered as
    __kp_symbol_su_guard_add_prefix / ..._del_prefix). 8 slots × 63 bytes,
    published with release/acquire semantics, add is idempotent, -ENOSPC
    when full.
  • App/module channel: CONTROL_FEATURE supercall with name
    "guard_add:/prefix" / "guard_del:/prefix".
    • Default OFF — requires the opt-in file /data/adb/ap/guard_ext to
      exist, otherwise -EPERM. KPM export users are not affected by the
      file gate (module trust already implies kernel-code execution).
    • Danger denylist: a prefix that covers an app-survival root
      (/data, /data/data, /storage, /system, …) is rejected with
      -EPERM (trailing-slash normalized; coverage checked on component
      boundary, so /data rejects but /data/adbfoo is fine).

Compatibility

  • Some OEM kernels (PJZ110/PGEM10-class) do not fire the getname wrap. On
    those devices the guard is inert and native DAC EACCES remains uniform —
    no regression.
  • Authorized callers (root, su whitelist, managers) see unchanged behavior.

Testing

  • Cross-build (WSL, aarch64-none-elf): kernel/kpimg links clean, 297008 B;
    nm confirms su_guard_add_prefix / su_guard_del_prefix exported and
    present in the __kp_symbol_* table.
  • Device (OnePlus PKX110, Android 15, 4.19.157-perf+), unprivileged probe
    via run-as (real app domain):
    • stat /data/adb/modules and siblings → EACCES (previously ENOENT
      from a hide module), matching stock semantics.
    • dmesg shows rate-limited adb guard lines for the blocked probes.
    • su flows and manager handshake unaffected.

Limitations

  • Relative-path probes: the guard matches the resolved string from the
    getname wrap, so chdir("/data") followed by stat("adb/...") never matches
    the absolute prefix. Stock DAC still answers uniformly, but a hide layer that
    returns ENOENT for hidden paths keeps an errno oracle open through relative
    probes. cwd-aware matching is possible future work.
  • io_uring (5.1+): openat/statx submitted through io_uring resolve
    paths in worker context and do not go through the getname wrap; the guard is
    inert for those ops on kernels that have io_uring.

Notes for reviewers

  • The getname wrap is the only pre-lookup interception point that covers all
    callers (it bypasses the dispatcher gate by design — this is required for
    oracle defense; authorization is still checked per-call).
  • The file-gated supercall channel keeps the default attack surface at zero:
    an attacker who cannot already write /data/adb cannot register prefixes.

…gistration

Unified EACCES before lookup at the getname wrap (name-match first, rate-limited logging), getattr insurance layer, KP_EXPORT_SYMBOL add/del prefix API (8 slots), file-gated supercall channel and danger-prefix denylist.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Registry correctness, absolute-path bypasses, and root-access regressions remain unresolved.

4 open issues
What changed in this PR

Adds pre-lookup path guarding to KernelPatch’s su compatibility layer to reduce errno-based detection of hidden /data/adb children.

Changes:

  • Registers a getname hook to return uniform EACCES for unauthorized guarded-path probes.
  • Adds dynamic prefix registration through kernel exports and an opt-in supercall channel.
  • Extends the optional getattr guard and initializes path-resolution support.
File Description
kernel/​patch/​common/​sucompat.c Implements path guards, prefix registration, and hook wiring.

Review effort: Balanced

if (free_slot < 0) free_slot = i;
continue;
}
if (!strncmp(s->prefix, prefix, SU_GUARD_PREFIX_LEN)) {
Comment on lines +583 to +596
static bool su_is_guarded_path(const char *name)
{
int i;
if (su_is_adb_prefix(name)) return true;
for (i = 0; i < SU_GUARD_SLOT_MAX; i++) {
struct su_guard_slot *s = &su_guard_slots[i];
/* ACQUIRE pairs the writer's RELEASE publication: a RELAXED reader can
* observe valid=1 before the memcpy'd prefix, see the zeroed slot, and
* (empty prefix matches every absolute path) storm EACCES system-wide. */
if (!__atomic_load_n(&s->valid, __ATOMIC_ACQUIRE)) continue;
if (su_guard_match(name, s->prefix)) return true;
}
return false;
}
/* Name match first, granted check only on the rare hit: the common path
* stays pure string compares, so the guard adds no kstorage-dependent
* per-call cost that a timing probe could latch onto. */
if (su_is_guarded_path(name)) {
* stays pure string compares, so the guard adds no kstorage-dependent
* per-call cost that a timing probe could latch onto. */
if (su_is_guarded_path(name)) {
if (is_su_allow_uid(uid) || is_trusted_manager_uid(uid)) return;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants