Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
911b10f
ksud: Let external Zygisk providers win and drop module install gating
matsuzaka-yuki Oct 14, 2026
1cd0a2f
manager: Derive supporting text colour from the card content
matsuzaka-yuki Oct 14, 2026
6ffc17e
nomount: Vendor the kernel module and build it with the DDK matrix
matsuzaka-yuki Oct 14, 2026
93faafb
nomount: Add the built-in backend, rule generation and visibility sync
matsuzaka-yuki Oct 14, 2026
c571cb4
manager: Add the NoMount page, provider switch and mode migration
matsuzaka-yuki Oct 14, 2026
da6a153
docs: Describe the built-in NoMount backend and its mount modes
matsuzaka-yuki Oct 14, 2026
ede5a16
nomount: Keep kernel-created virtual directories and label them
matsuzaka-yuki Oct 14, 2026
2793d43
nomount: Report the injected rule count and separate it from the kern…
matsuzaka-yuki Oct 14, 2026
5b50754
manager: Match the NoMount page top bar to the app design
matsuzaka-yuki Oct 14, 2026
54f1567
nomount: Add per-module hot load, unload, rule counts and skip mount
matsuzaka-yuki Oct 14, 2026
191b207
manager: Add the NoMount module list and app exclusion picker
matsuzaka-yuki Oct 14, 2026
de685f0
nomount: Defer to an installed NoMount module and guard metamodule in…
matsuzaka-yuki Oct 14, 2026
6b8bf61
manager: Add clear-everything and follow-profile actions to NoMount
matsuzaka-yuki Oct 14, 2026
0bd80b2
nomount: Handle a kernel built-in driver and record state changes
matsuzaka-yuki Oct 14, 2026
a827e5a
nomount: Explain why the driver is unusable and fix dead diagnostics
matsuzaka-yuki Oct 14, 2026
883041a
manager: Spell out how NoMount changes file visibility
matsuzaka-yuki Oct 14, 2026
bbcaae8
docs: Correct the NoMount visibility description
matsuzaka-yuki Oct 15, 2026
dba71b6
manager: Stop claiming the NoMount exclusions follow profiles
matsuzaka-yuki Oct 15, 2026
f8d1e7e
nomount: Give build-lkm.sh a shebang
matsuzaka-yuki Oct 15, 2026
8eeb221
build: Silence the ShellCheck findings in build.sh
matsuzaka-yuki Oct 15, 2026
6c921df
manager: Fix the lock screen night text and PIN keyboard
matsuzaka-yuki Oct 15, 2026
2aa52ca
fix(actions): Build nomount.ko from the checkout path
matsuzaka-yuki Oct 15, 2026
b908303
manager: Fix the lint error in the NoMount confirm dialog
matsuzaka-yuki Oct 15, 2026
b00df7b
manager: Fix the lint error the plugin store notice reports
matsuzaka-yuki Oct 15, 2026
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
13 changes: 12 additions & 1 deletion .github/workflows/ddk-lkm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,22 @@ jobs:
llvm-strip -d "$OUT_DIR/$OUTPUT_NAME"
echo "Size after stripping: $(du -h "$OUT_DIR/$OUTPUT_NAME" | cut -f1)"

- name: Build nomount.ko
run: |
echo "=== Building NoMount for KMI: ${{ inputs.kmi }} ==="
# The checkout lives at $GITHUB_WORKSPACE; /github/workspace is only the
# scratch directory the kernelsu.ko step creates for its artifacts.
NOMOUNT_OUT="/github/workspace/out/aarch64/${{ inputs.kmi }}_nomount.ko" \
sh "$GITHUB_WORKSPACE/userspace/nomount/build-lkm.sh"
echo "Size: $(du -h "/github/workspace/out/aarch64/${{ inputs.kmi }}_nomount.ko" | cut -f1)"

- name: Upload kernelsu.ko artifact
uses: actions/upload-artifact@v7
with:
name: aarch64-${{ inputs.kmi }}-lkm
path: /github/workspace/out/aarch64/${{ inputs.kmi }}_kernelsu.ko
path: |
/github/workspace/out/aarch64/${{ inputs.kmi }}_kernelsu.ko
/github/workspace/out/aarch64/${{ inputs.kmi }}_nomount.ko

- name: Build kernelsu.ko for x64
env:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ksud-extra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
if: ${{ inputs.pack_lkm }}
run: |
cp aarch64-android*-lkm/*_kernelsu.ko ./userspace/ksud/bin/aarch64/
cp aarch64-android*-lkm/*_nomount.ko ./userspace/ksud/bin/aarch64/
cp x86_64-android*-lkm/*_kernelsu.ko ./userspace/ksud/bin/x86_64/

- name: Prepare ksuinit
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ksud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
if: ${{ inputs.pack_lkm }}
run: |
cp aarch64-android*-lkm/*_kernelsu.ko ./userspace/ksud/bin/aarch64/
cp aarch64-android*-lkm/*_nomount.ko ./userspace/ksud/bin/aarch64/
cp x86_64-android*-lkm/*_kernelsu.ko ./userspace/ksud/bin/x86_64/

- name: Prepare ksuinit
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Standard KernelSU modules are managed from the manager. A single active metamodu

### FolkZygisk

FolkZygisk is a built-in Zygisk engine. The manager exposes its installation and activation controls, and `ksud` handles provider registration without conflicting with an existing Zygisk provider.
FolkZygisk is a built-in Zygisk engine. The manager exposes its installation and activation controls, and `ksud` steps the built-in engine aside whenever an external Zygisk provider is active, so provider modules install normally.

### SUSFS

Expand All @@ -59,6 +59,12 @@ SUSFS management is capability-gated. The controls appear only on kernels that s

Folk-Mount is the built-in module mount engine. It can run in auto, built-in, or metamodule mode.

### NoMount

NoMount is the built-in VFS redirection backend: instead of mounting anything it takes over path resolution and directory iteration in a kernel module, so `/proc/mounts` stays clean. The module ships inside ksud (embedded per KMI, built by this repository); the backend needs neither a metamodule nor the `nm` binary.

The manager exposes the driver state, the number of active rules, visibility exclusions and a "mounting off" mode; the backend can be set to automatic, built-in Folk Mount, metamodule only, NoMount or off. Choosing NoMount falls back to the built-in engine when the driver is unavailable and says so on the settings page. Visibility is an exception list kept by ksud: every app sees the injected files until it is added, and the NoMount page copies each app profile's "unmount modules" verdict into that list on demand, so later profile changes need another copy.

## Security

The manager authenticates itself by APK signature: only the trusted manager and apps the user has explicitly claimed can manage the kernel. Root access is whitelist-based, so only granted apps can reach `su`. See [SECURITY.md](./SECURITY.md) for vulnerability reporting.
Expand Down
8 changes: 7 additions & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ FinaSU 提供:

### FolkZygisk

FolkZygisk 是内置的 Zygisk 引擎。管理器提供安装与激活控制,`ksud` 负责注册 provider,并与已有的 Zygisk provider 互不冲突。
FolkZygisk 是内置的 Zygisk 引擎。管理器提供安装与激活控制;已有外置 Zygisk provider 时 `ksud` 会让内置引擎自动让位,因此 provider 模块可正常安装。

### SUSFS

Expand All @@ -59,6 +59,12 @@ SUSFS 管理按能力开关。仅在支持的内核上显示相关控件,每

Folk-Mount 是内置的模块挂载引擎,可在自动、内置或 metamodule 模式间切换。

### NoMount

NoMount 是内建的 VFS 路径重定向后端:不产生任何挂载,而是由内核模块直接接管路径解析与目录遍历,因此不会污染 `/proc/mounts`。内核模块随 ksud 一起分发(按 KMI 内嵌,由本仓库自行编译),后端本身不依赖模块或 `nm` 二进制。

管理器提供驱动状态、注入规则数、可见性例外与「关闭挂载」;挂载模式可选 `自动 / 内建 Folk Mount / 仅 metamodule / NoMount / 关闭`。选择 NoMount 时若驱动不可用会自动回退到内建引擎,并在设置页说明原因。可见性是一份由 ksud 保存的例外名单:名单内的应用看到原厂文件系统,其余应用都能看到注入的文件;NoMount 页可以按需把各应用 Profile 的「卸载模块」判定复制进这份名单,改动 Profile 后需要再复制一次。

## 安全提示

管理器通过 APK 签名验证自身身份:只有受信任的管理器以及用户显式认领的应用才能管理内核。Root 权限基于白名单,只有被授权的应用才能访问 `su`。漏洞上报请参阅 [SECURITY.md](./SECURITY.md)。
Expand Down
53 changes: 50 additions & 3 deletions build.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
#!/usr/bin/env bash
# ANDROID_NDK_HOME is exported inside a subshell on purpose: every build stage
# gets it for its own cargo invocation and the parent shell keeps none of it, so
# ShellCheck's subshell-scope notes (SC2030/SC2031) do not apply here.
# shellcheck disable=SC2030,SC2031
#
# FolkSU build driver - a local mirror of the GitHub Actions pipeline.
#
Expand Down Expand Up @@ -55,8 +59,12 @@ have() { command -v "$1" >/dev/null 2>&1; }

read_keystore_cfg() {
[[ -f "$KEYSTORE_PROPS" ]] || die "missing $KEYSTORE_PROPS (copy sign.example.properties and fill it in)"
set -a
# The keystore file is a user-provided path, so its content cannot be
# analysed up front.
# shellcheck disable=SC1090
set -a; source "$KEYSTORE_PROPS"; set +a
source "$KEYSTORE_PROPS"
set +a
: "${KEYSTORE_FILE:?KEYSTORE_FILE not set in $KEYSTORE_PROPS}"
: "${KEYSTORE_PASSWORD:?KEYSTORE_PASSWORD not set in $KEYSTORE_PROPS}"
: "${KEY_ALIAS:?KEY_ALIAS not set in $KEYSTORE_PROPS}"
Expand Down Expand Up @@ -139,6 +147,42 @@ build_lkm() {
done
}

# NoMount's kernel module is vendored at userspace/nomount (GPL-2.0-only, see
# userspace/nomount/UPSTREAM.md) and built out-of-tree with the same DDK matrix
# as kernelsu.ko. The stripped module is staged into ksud's embedded assets as
# <kmi>_nomount.ko; it is never flashed and has no standalone module zip.
build_nomount() {
have docker || die "docker is required to build the NoMount LKM"
mkdir -p userspace/ksud/bin/aarch64 userspace/nomount/build

local kmi image rel dest
for kmi in $KMIS; do
image="ghcr.io/ylarod/ddk-min:${kmi}-${DDK_RELEASE}"
log "building NoMount LKM for $kmi ($image)"
docker image inspect "$image" >/dev/null 2>&1 || docker pull "$image"

# The container sees the repository mounted at /build, so the output
# path has to be given in container coordinates.
rel="userspace/nomount/build/${kmi}_nomount.ko"
dest="$REPO_ROOT/$rel"
rm -f "$dest"
docker run --rm --privileged \
-v "$REPO_ROOT":/build \
-e CONFIG_NOMOUNT=m -e CC=clang -e NOMOUNT_OUT="/build/$rel" \
"$image" sh /build/userspace/nomount/build-lkm.sh

[[ -f "$dest" ]] || die "NoMount LKM for $kmi was not produced"
cp "$dest" userspace/ksud/bin/aarch64/
log "staged userspace/ksud/bin/aarch64/${kmi}_nomount.ko"

# The container runs as root, so hand the artifacts back to us.
docker run --rm -v "$REPO_ROOT/userspace/nomount":/n \
--entrypoint /bin/sh "$image" \
-c "chown -R $(id -u):$(id -g) /n" >/dev/null 2>&1 || \
warn "could not restore ownership of userspace/nomount/ (artifacts may be root-owned)"
done
}

abi_to_triple() {
case "$1" in
arm64-v8a) echo aarch64-linux-android ;;
Expand Down Expand Up @@ -254,11 +298,13 @@ usage: ./build.sh <step> [step...]
steps:
lkm build the kernel module(s) via the DDK docker image, stage them
into userspace/ksud/bin/aarch64/ so ksud embeds them
nomount build the vendored NoMount module(s) the same way, stage them as
userspace/ksud/bin/aarch64/<kmi>_nomount.ko for ksud to embed
ksuinit cross-compile the ksuinit helper ksud embeds (needed by boot-patch)
ksud cross-compile the ksud daemon with cargo-ndk
apk gradle assembleRelease (signed with manager/keystore.properties)
repack inject the freshly built ksud into the APK and re-sign it
all lkm -> ksuinit -> ksud -> apk -> repack
all lkm -> nomount -> ksuinit -> ksud -> apk -> repack

environment overrides:
KMIS=android15-6.6 kernel modules to build
Expand All @@ -278,11 +324,12 @@ main() {
for step in "$@"; do
case "$step" in
lkm) build_lkm ;;
nomount) build_nomount ;;
ksuinit) build_ksuinit ;;
ksud) build_ksud ;;
apk) build_apk ;;
repack) build_repack ;;
all) build_lkm; build_ksuinit; build_ksud; build_apk; build_repack ;;
all) build_lkm; build_nomount; build_ksuinit; build_ksud; build_apk; build_repack ;;
-h|--help|help) usage ;;
*) die "unknown step: $step (try --help)" ;;
esac
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import org.json.JSONObject
enum class FolkMountMode(val token: String) {
AUTO("auto"),
BUILTIN("builtin"),
METAMODULE("metamodule");
METAMODULE("metamodule"),
NOMOUNT("nomount"),
OFF("off");

companion object {
fun fromToken(token: String?): FolkMountMode? =
Expand All @@ -25,7 +27,9 @@ enum class FolkMountMode(val token: String) {
*/
enum class FolkMountProvider(val token: String) {
BUILTIN("builtin"),
METAMODULE("metamodule");
METAMODULE("metamodule"),
NOMOUNT("nomount"),
NONE("none");

companion object {
fun fromToken(token: String?): FolkMountProvider? =
Expand All @@ -34,14 +38,14 @@ enum class FolkMountProvider(val token: String) {
}

/**
* Decoded `ksud mount status --json` payload (`schema_version = 1`).
* Decoded `ksud mount status --json` payload (`schema_version` 1 or 2).
*
* Property names map one-to-one to the ksud JSON keys:
* [configuredMode] <- `configured_mode`, [nextProvider] <- `next_provider`,
* [metamoduleId] <- `metamodule_id`, [metamoduleEnabled] <- `metamodule_enabled`,
* [bootId] <- `boot_id`, [bootProvider] <- `boot_provider`,
* [bootResult] <- `boot_result`, [targetCount] <- `target_count`,
* [error] <- `error`.
* [bootWarning] <- `boot_warning` (schema 2), [error] <- `error`.
*/
@Immutable
data class FolkMountStatus(
Expand All @@ -53,10 +57,14 @@ data class FolkMountStatus(
val bootProvider: FolkMountProvider?,
val bootResult: String,
val targetCount: Int?,
/** Provider-specific degradation for this boot, e.g. a NoMount fallback. */
val bootWarning: String?,
val error: String?,
) {
companion object {
const val SCHEMA_VERSION = 1
/** Newest schema this app understands; older schemas stay readable. */
const val SCHEMA_VERSION = 2
private const val OLDEST_SCHEMA_VERSION = 1

/**
* Decode a `ksud mount status --json` object.
Expand All @@ -66,6 +74,10 @@ data class FolkMountStatus(
* than falling back to a default mode.
*/
fun fromJson(json: JSONObject): FolkMountStatus {
val schema = json.optInt("schema_version", -1)
check(schema in OLDEST_SCHEMA_VERSION..SCHEMA_VERSION) {
"unsupported folk mount schema_version $schema"
}
val configuredMode = FolkMountMode.fromToken(json.optStringOrNull("configured_mode"))
?: error("folk mount status has no valid configured_mode")
return FolkMountStatus(
Expand All @@ -77,6 +89,7 @@ data class FolkMountStatus(
bootProvider = FolkMountProvider.fromToken(json.optStringOrNull("boot_provider")),
bootResult = json.optString("boot_result", "unknown"),
targetCount = json.optIntOrNull("target_count"),
bootWarning = json.optStringOrNull("boot_warning"),
error = json.optStringOrNull("error"),
)
}
Expand Down

This file was deleted.

This file was deleted.

Loading
Loading