Isolated-process mount-view detection - #24
Open
JingMatrix wants to merge 7 commits into
Open
Conversation
The CI signs the release build itself with the repository secrets, so the release build type must not attach the debug signing config, which named the output app-release.apk and broke the rename-then-sign step.
Kernel-side mount hiders (e.g. KernelSU's mount_hide) erase module mount
records from /proc/<pid>/{mountinfo,mounts,mountstats} at the seq_file layer,
keyed on the reader, so every mount view an app reads is consistently scrubbed
-- defeating the marker scan, the cross-view differential, cross-file and
self-vs-init checks. The stat syscall family is not on that path, so it is
ground truth.
New shared core (recon.cpp / librecon.so), run identically in the main process
(libdemo), the native isolated probe (libmain) and the classic isolated Java
probe (ProcScanner via JNI):
- Reconciliation: statx(STATX_ATTR_MOUNT_ROOT) + statfs vs mountinfo. A path the
kernel confirms as a mount root, or a single file backed by the userdata fs
where a read-only partition is expected, that mountinfo omits is a hidden
record. Immune to the filter and needs no other process.
- Structural (mountinfo-only, fire even where the hidden path is unreachable):
orphaned parent id, and a gap in the peer-group id run collected as
shared:N u master:N so it is dense in both a global and an app slave view.
Fix mount-trace false positives on clean devices:
- native mount.cpp: drop the mount-id arithmetic heuristics (root.id==parent+1,
file-order-consecutive ids) that flagged clean phones and varied run to run;
keep the root-tool source/path scan and move the peer-group check into recon.
- classic differential: normalize out the per-process app-data isolation tmpfs
before comparing views, and only flag distinct > classes (injection adds a
view; fewer is benign).
- markers: drop data_mirror (stock AOSP tree) and the generic overlay-option
words; anchor the modules marker to /adb/modules so it no longer matches OEM
paths such as com.oplus.moduleservices.
UI: split the main-process integrity checks into two families -- Injection
detections and Mount traces -- driven by a new per-check "type" field, with the
detailed reconciliation card (findings + probes) under the mount family.
dex2oat binds are intentionally not probed: SELinux denies apps getattr on
dex2oat_exec so stat cannot reach them, and they never enter the app namespace.
…c probe
Differential panel: each mount-view group now carries the processes it compares
-- pid, uid, propagation, and the full cmdline -- and the panel renders them as a
per-view table so a distinct-views-vs-classes mismatch shows WHICH processes
diverge, not just raw pids. The differing-records list is labelled A (largest
view) / B (2nd largest).
Full command line: display /proc/pid/cmdline (argv[0]) instead of the kernel's
15-char-truncated comm, in the differential samples and the per-process mount
cards; MonoBlock scrolls so long names are not cut.
Speed up the classic isolated probe (~2.4s -> ~0.8s on ~840 visible pids),
without changing any verdict input (recon / markerHits / differential / crossFile
/ selfVsInit all come from readable-mountinfo pids, still fully read):
- read comm/cmdline only for pids with a readable mount view (the only consumers);
the hundreds of unreadable pids skip those two dead reads.
- uid from stat("/proc/pid").st_uid instead of reading and parsing status.
- read mountstats for self only (every other pid is a guaranteed EACCES and its
differential is always a single view).
- sample 40 pids in the fileAccess aggregate (a hidepid-leak diagnostic, not a
verdict input) instead of testing every pid -- tens of thousands of open() calls
removed; result carries tested/visiblePids and a sampled flag.
- do not format+log a line for every unreadable pid.
The Privisolated differential was normalizing benign per-process mounts out BEFORE counting views, so a genuine two-view split (e.g. a WebView sandboxed process lacking the /storage FUSE+tmpfs view a permitted app has) was reported as a single view. That baked our judgment into the raw data the UI shows. Now the raw grouping keeps every mount -- distinctViews is the true count and the panel shows both groups and the exact differing records. The benign judgment is a separate layer: interpretedViews is computed by discounting the mounts Android customises per process (app-data isolation tmpfs and the external-storage subsystem -- /storage and the storage /mnt binds, matched by mountpoint since their fs type varies tmpfs/fuse/sdcardfs), and ONLY that interpreted count drives the verdict (interpreted > classes). A raw difference that collapses once those are removed is flagged benignDifference, not a mismatch. - FileView carries normalized (raw) and normalizedInterpreted. - differentialByFile groups on raw for display, counts interpreted for the verdict, and tags each differing record benign via isBenignKey. - UI shows views as raw->interpreted, a benign result state, and per-record [benign]/[ LEAK ] tags, so the raw truth and our reading are both visible; a non-benign record keeps interpreted > classes and still fires as a LEAK.
…lass pair 7c817ae keyed the verdict on interpretedViews -- raw views minus the storage/data mounts Android customises per process -- on the theory that a same-class view difference is benign per-process variation. That is unsound: Privisolated's invariant is that processes in one propagation class have an identical view, so ANY same-class difference IS the inconsistency, and a module can mount under the very /storage, /mnt, /data/user paths the interpreted layer strips, so discounting them by mountpoint just deletes the evidence. On a stock device two views arise only from two classes (Chrome's isolated services shared:1 vs the WebView zygote master:1); a third view within one class does not. The verdict now fires on rawMismatch = (classes > 0 && distinctViews != classes) -- Privisolated's actual rule, both directions, with classes==0 (no shared:/master: root seen) treated as not-applicable rather than an unconditional mismatch. The interpreted / mismatch / benignDifference machinery and isPerProcessBenignMount are gone; the storage/data predicate survives only to tag a differing record's shape for display. The surfaced records now come from a SAME-class pair: the diff buckets the distinct views by class and, when a class holds more than one view, diffs two of them, so the panel shows the master:1-vs-master:1 leak instead of the normal shared-vs-master delta the two-largest heuristic used to surface. View rows render largest-first and the two diffed views are tagged (A)/(B), so the A-only / B-only records are attributable to a process group. The verdict reason lists per-file counts, e.g. [mountinfo(3/2), mounts(3/2)], and no longer names a cause -- that stays in comments.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This variant detects root and module mounts from isolated processes, where a normal app cannot look. It ports two techniques, each credited to its original proof-of-concept:
LSPosed/Privisolated): isolated processes inherit theAID_READPROCgroup, so it reads the/proc/<pid>/{mountinfo,mounts,mountstats}of every peer it is permitted to, scans each record for module markers, and runs the propagation-class differential Privisolated relies on;zygote_nextservice, after ZygoteNextProbe (xiaotong6666/ZygoteNextProbe): forked into init's global mount namespace on Android 17, it reads its own/proc/self/mountinfo, where module mounts remain visible.The dashboard is rebuilt in Jetpack Compose — foldable sections, a raw-data report and a logs panel, and export of either. The existing native toolkit (solist/vmap injection,
/systemremount, mountinfo) is folded in as a main-process integrity section, so every check reads from one screen. A file-access sweep over all visible pids records which sensitive/proc/<pid>entries an isolated reader can actually open, confirming SELinux confines the reach to a handful of same-domain peers.