Repository navigation
nomount: Add a built-in NoMount backend - #43
Merged
Merged
Conversation
Provider detection no longer gates module installation or activation: ksud installs archives through the upstream path, with no Zygisk lock and no conflict or marker checks. An active external Zygisk provider simply takes precedence, and the built-in engine steps aside at boot by reading the provider set before touching the boot guard. The Manager drops preflight validation and reports the effective provider instead. Enable persists the configuration before scanning providers, so a failing scan can no longer turn a completed enable into a CLI error.
NoMount is a VFS path redirection module; FolkSU intends to use it as a built-in module injection backend, so its kernel sources are vendored here instead of being downloaded prebuilt from upstream releases. - Vendor kernel/src (GPL-2.0-only) at 7046c7a (v2.1.1) plus UPSTREAM.md, NOTICE and both upstream license texts; imported files stay byte-identical. - build.sh nomount builds it out-of-tree with the same DDK image matrix as kernelsu.ko and stages <kmi>_nomount.ko into ksud's embedded assets. The build runs from a temporary copy so the vendored sources are never written to. - ddk-lkm.yml builds and uploads the module with the kernelsu.ko artifact; ksud.yml/ksud-extra.yml stage it before building ksud. - assets.rs exposes the KMI set that ships a NoMount module. Verified locally against DDK 20260828: android12-5.10 (70000 B), android15-6.6 (59168 B) and android17-6.18 (70584 B) all build and report version=21, license=GPL.
ksud can now drive NoMount on its own, without the upstream metamodule or the `nm` binary: the kernel protocol is reimplemented in Rust and the rules come from the module directory instead of metamount.sh. - nomount.rs encodes the page-aligned add_key payload, probes the driver, refuses an API below 21, and verifies every applied rule by reading the kernel list back, so a transport that drops rules cannot look successful. - nomount_plan.rs walks the modules (partition remapping, whiteout markers, opaque directories) and replaces a replaced directory by redirecting it: upstream whiteouts it without the directory flag and the kernel then rejects every child rule with -ENOTDIR, so children would be lost. - Visibility: the stored UID list and the isolated-process flag are replayed into the kernel, which is what hides injected files from apps now that there are no mounts to unmount. - magic_mount gains the nomount provider: it loads the embedded module for the running KMI, falls back to the built-in engine when NoMount cannot be made usable, and records why. `mount status` becomes schema 2 with boot_warning. - New commands: ksud nomount status/inject/list/clear and uid set/add/del/isolated. Host tests: 26 new cases covering the wire layout, batching, read-back verification, rule generation, the visibility diff and the kernel's last-rule-wins conflict rule.
The NoMount backend is now visible and controllable, and the default backend moves to NoMount with the built-in engine as its fallback. - NomountScreen reports the driver, the injection and the visibility state. Every row that can look healthy is gated on the driver answering, and the read state is never replaced by an invented fallback. - FolkMountStatus accepts schema 1 and 2 and reads boot_warning, so a stale ksud or a stale app degrades to one unavailable row instead of a hard failure. - New mount modes: nomount (NoMount, falls back to the built-in engine) and off (inject nothing). The settings dropdown, provider labels and strings follow. - A missing mount_mode now means: keep the previous behaviour when this device already mounted modules (upgrade), otherwise select nomount, and record the decision so the default applies only once. Verified with ./gradlew :app:compileDebugKotlin and :app:testDebugUnitTest. Not included: pushing App Profile "unmount modules" into the UID list, which needs an enumeration API the manager does not have yet.
Also apply rustfmt to the provider dispatch.
Device testing showed the kernel creates intermediate directories to host rules whose parent path does not exist, and that reconcile treated those as stale: it deleted them and the kernel rebuilt them on the next add, reporting a pointless rules_removed=3 on every injection. - reconcile_rules skips rules flagged as kernel-created virtual directories. - Rule carries the virtual_dir flag, nomount list prints "(virtual dir)" and the JSON output exposes it. Verified on a real device (CPH2723, android15-6.6): two consecutive injections now report rules_removed=0 with the kernel's three virtual directories intact.
…el total Device testing showed the NoMount provider left `target_count` empty in `ksud mount status`, and that the two commands disagreed on the rule count: `nomount status` reported everything the kernel holds (including the intermediate directories the kernel creates itself), while `mount status` reported what we published. - The per-boot record now carries the count for every provider that knows it, which is every provider except the metamodule's opaque script. - `nomount status` reports `rules_applied` for the rules we published and `kernel_rules` for the kernel total, so the manager and the CLI agree. Verified on a real device: mount status shows target_count=101 and nomount status shows rules_applied=101 with kernel_rules=104.
The page used a plain Scaffold with a compact TopAppBar and a bare arrow icon, which does not match the other pages. It now uses the shared ExpressiveScaffold, LargeFlexibleTopAppBar, TopBarBackButton and expressiveTopAppBarColors, with a Refresh action like the Susfs and Sulog pages have. Checked on device: the bar collapses while scrolling and the back button keeps the shared styling.
The upstream metamodule UI could inject or drop a single module's rules without rebooting, and showed how many files each module injects. Both are now available to the built-in backend. - nomount_plan exposes per-module collection, including a variant that ignores disable/skip marks because a module that was just turned off still has the paths that need removing. - `ksud nomount hot-load|hot-unload <id>` and per-module counts plus the UID arrays in `nomount status` (schema 2). - `ksud module skip <id> [--off]` writes the skip_mount marker the planners already honour. Verified on device: hot-unload takes the module from 101 rules to 0, hot-load restores 101, and the skip marker toggles.
Closes the two features the upstream metamodule WebUI had and the built-in page lacked. - The NoMount page lists every module that contributes rules with its rule count; tapping a module injects it immediately and the trailing button drops it until the next reboot. - "Excluded apps" opens a picker over the installed apps (search, show system apps, per-row checkbox) whose selection is saved in one go, plus a clear action on the page. The exclusion list is what hides injected files from an app now that nothing is mounted. - NomountStatus reads schema 1 and 2 and carries the module and UID lists. Verified on device: selecting two apps persists uids 10370/10496 into the kernel policy, clearing empties it, and the module list renders with counts.
…stalls Two writers on the same paths is the one conflict the built-in backend cannot resolve: the kernel keeps the last rule written. Both sides are now handled. - A metamodule that ships its own NoMount tool or LKM owns injection: the backend detects it, reports it as `deferred_to` in `nomount status`, refuses to write rules itself, and at boot hands the mount stage to that module. - Installing a metamodule while the built-in backend is the injector is refused with a message that says what to change, because that metamodule would inject on its own. - `nomount inject --dry-run --verbose` prints every rule and skip reason, which is what the comparison against upstream's metamount.sh needs. Verified on device with a stub metamodule: deferred_to=nomount-stub, owns_injection=false, inject and hot-load both refuse with exit 1, and a regular module still installs normally.
- "Clear everything" drops the rules and the exclusions in one action, with a summary that says it lasts until the next reboot. - The exclusion picker can select the apps whose profile hides modules from them, which is the same decision the kernel already answers per UID; it honours the picker's own visibility (system apps only when shown) and the result is visible as a count before saving. - The driver card explains when an installed module owns injection instead of this backend. Verified on device: clear-everything takes the rules to 0 from the UI and the follow action selects the listed apps (168 of them on this device), with no write until Save.
A kernel with CONFIG_NOMOUNT=y needs no load at all: everything here talks to the key API, and the probe already answers, so the built-in path worked. Two things were missing around it. - An outdated driver (built-in or an LKM someone else loaded) is no longer followed by a load attempt: a registered key type always makes a second registration fail, so the load could only produce a misleading error. The backend now reports the version it found and refuses to inject. - `nomount status` reports `driver_kind` (builtin or module) and the last action that changed kernel state, so "who emptied the rules" does not depend on the kernel ring buffer. The manager shows the kind next to the version. Verified on device: driver_kind=module, and last_action flips from empty to inject after an injection.
A kernel whose KMI this build ships no module for (legacy or custom kernels) already fell back to the built-in engine correctly, but the reason only appeared in the boot record and the page said "unavailable" with nothing behind it. - `nomount status` reports `unavailable_code` (no_asset_for_kmi, outdated, not_loaded, unknown_kmi) with the KMI or version it refers to. - The NoMount page and the settings row render that as a localized sentence that says what to do, for example "use Automatic or Built-in Folk Mount". - The reason was computed after the early return for an unusable driver, so it was dead code: it now runs before the return. Verified on device by building a ksud without the android15-6.6 module: the boot record names the missing asset, the CLI reports no_asset_for_kmi/android15-6.6, and the page shows the actionable sentence, while the built-in engine keeps injecting the modules.
Switching to NoMount flips who can see the injected files: the built-in Folk Mount hides them from apps whose profile says "unmount modules" (every non-root app by default), while NoMount starts with no exclusions at all. That was implicit before. - Selecting NoMount now asks for confirmation first and only writes the mode after the user agrees. The dialog names the change, why it happens and what to do to keep the old behaviour. - The NoMount page says it in two places: the exclusion row reads "every app can see the injected files" when the list is empty, and a Default visibility row explains the difference from Folk Mount. Verified on device: the dialog gates the write (the mode file stays unchanged until confirm), confirming switches to nomount, and both rows render.
Both READMEs said visibility follows each app profile's "unmount modules" setting. It does not: ksud applies a fixed exception list it stores itself, and the app profiles are copied into that list on demand from the NoMount page. Say what actually happens, including that a later profile change needs another copy.
The exclusion row caption asserted the list tracks app profiles on its own; only the "Follow app profiles" action copies them, and nothing re-syncs afterwards. The caption now describes that copy, and the action summary says the profiles are not tracked.
ShellCheck fails the workflow with SC2148 because the file is run as "sh build-lkm.sh" and carries no shebang, so the tool cannot pick a target shell.
The ShellCheck job has been red on master as well, for two reasons. The SC1090 directive sat on a line holding three commands, so it covered "set -a" instead of the source. The SC2030/SC2031 notes come from the deliberate ( export ANDROID_NDK_HOME=... ) subshells, which is the intended scoping. Give each one the directive it needs; no behaviour change.
The full-screen prompt painted its text straight on a background, so the headline kept Material's default black content colour and vanished on a dark surface. Wrapping the screen in a Surface makes its content follow onSurface. The PIN field keeps focus after the keyboard is dismissed, so tapping a cell only re-requested focus and the pad read as read-only. Ask the IME back on tap, and again whenever the pad is re-enabled after a rejected attempt.
The job's checkout is mounted at $GITHUB_WORKSPACE, while /github/workspace only holds the artifacts the kernelsu.ko step creates there. Asking sh for the script under /github/workspace exited 127 on every KMI, and matrix fail-fast then cancelled the remaining jobs of the run.
The dialog took its strings from LocalContext, which Android lint rejects (LocalContextGetResourceValueCall: the value is not configuration-aware) and which aborts :app:lintDebug for every build. Read them through LocalResources, as the rest of the app does.
The same rule fires on the notice shown after a plugin install, and it fails the Build Manager job on master as well, so the release pipeline cannot get past :app:lintDebug. Same fix: query the notice through LocalResources.
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.
Adds NoMount as a third injection backend next to the built-in Folk Mount engine
and the metamodule path: instead of mounting anything it redirects path
resolution and directory iteration in a kernel module, so nothing shows up in
/proc/mounts.What is in here
userspace/nomount/vendors upstreammaxsteeel/nomount at
7046c7a(v2.1.1). The kernel sources stay byte-identical (hashes in
UPSTREAM.md) andbuild-lkm.shcompiles them from a temporary copy inside the DDK image, sore-syncing stays a mechanical diff. GPL-2.0-only, same as
kernel/.<kmi>_nomount.konext tokernelsu.koandksud embeds them as assets. There is no standalone module zip and nothing is
flashed.
add_keywith packed4096-byte payloads) and regenerates the rules instead of shelling out to
upstream's
metamount.sh/rule-paths.sh: probe (absent / outdated / ready,API >= 21), load from the embedded asset, apply, read back to verify,
per-module hot load and unload, UID visibility, isolated-process handling.
mount_modegainsnomount;auto,builtin,metamoduleandoffbehaveas before. A driver that cannot be used falls back to the built-in engine and
records why (
no_asset_for_kmi,outdated,not_loaded,unknown_kmi).app exclusion picker, a settings entry, and a confirmation dialog when
switching to NoMount, because that switch changes who can see the injected
files.
Review notes
userspace/nomount/kernel/**is upstream code, so please read it as a vendordrop rather than as a change of ours. Everything else, including the rule
generator, the protocol implementation and the manager surface, is new.
Device verification (OnePlus 13s, Android 16, 6.6.118 GKI, SUSFS active)
metamount.sh/rule-paths.shoutput for the installed module set. Onedeliberate difference: for a
.replace/ opaque directory upstream emits onerule per child (48 of which the kernel rejects with
-ENOTDIR), while weredirect the directory itself.
symlinks;
/proc/mountsstays clean.su <uid> -c.disable/enable sync, mode migration (
autofor upgraded devices,nomountfor fresh ones) and the
off/builtin/nomountreboot matrix.boot record carries a warning when the engine falls back.
NoMount frames in dmesg backtraces.
Checks
cargo ndk -t arm64-v8a checkandclippyclean, plus the CI shape:RUSTFLAGS=-Dwarnings cargo clippy --releasefor aarch64 and x86_64.cargo fmt --all --checkon stable and on nightly.cargo test -p ksud: 204 pass, 1 failure that pre-dates this branch(
lkm_image_btf::tests::rejects_conflicting_loading_module_values).compileDebugKotlin,testDebugUnitTest,assembleRelease.ddk-lkm.ymlruns for the first time in this PR;locally only 6.6 (the device KMI), 5.10 and 6.18 have been built so far.
Not verified
exercised on a device.
nomountis the active provider.Note
The first two commits (
911b10f2,1cd0a2f7) are pre-existing localmaincommits unrelated to NoMount; they are ancestors of this branch.