Build KernelSU as LKM (#1254)

Co-authored-by: weishu <twsxtd@gmail.com>
This commit is contained in:
Ylarod
2024-03-15 18:53:24 +08:00
committed by GitHub
parent e3998c0744
commit 7568d55be1
27 changed files with 1091 additions and 202 deletions

View File

@@ -134,4 +134,4 @@ jobs:
version_name: android12-5.10.177 version_name: android12-5.10.177
tag: android12-5.10-2023-06 tag: android12-5.10-2023-06
os_patch_level: 2023-06 os_patch_level: 2023-06
patch_path: "5.10" patch_path: "5.10"

View File

@@ -174,4 +174,4 @@ jobs:
version_name: android13-${{ matrix.version }}.${{ matrix.sub_level }} version_name: android13-${{ matrix.version }}.${{ matrix.sub_level }}
tag: android13-${{ matrix.version }}-${{ matrix.os_patch_level }} tag: android13-${{ matrix.version }}-${{ matrix.os_patch_level }}
os_patch_level: ${{ matrix.os_patch_level }} os_patch_level: ${{ matrix.os_patch_level }}
patch_path: ${{ matrix.version }} patch_path: ${{ matrix.version }}

View File

@@ -132,10 +132,13 @@ jobs:
- version: "5.15" - version: "5.15"
sub_level: 110 sub_level: 110
os_patch_level: 2023-09 os_patch_level: 2023-09
- version: "6.1"
sub_level: 68
os_patch_level: 2024-02
uses: ./.github/workflows/gki-kernel.yml uses: ./.github/workflows/gki-kernel.yml
with: with:
version: android14-${{ matrix.version }} version: android14-${{ matrix.version }}
version_name: android14-${{ matrix.version }}.${{ matrix.sub_level }} version_name: android14-${{ matrix.version }}.${{ matrix.sub_level }}
tag: android14-${{ matrix.version }}-${{ matrix.os_patch_level }} tag: android14-${{ matrix.version }}-${{ matrix.os_patch_level }}
os_patch_level: ${{ matrix.os_patch_level }} os_patch_level: ${{ matrix.os_patch_level }}
patch_path: ${{ matrix.version }} patch_path: ${{ matrix.version }}

View File

@@ -18,8 +18,20 @@ jobs:
matrix: matrix:
include: include:
- target: aarch64-linux-android - target: aarch64-linux-android
os: ubuntu-latest
- target: x86_64-linux-android - target: x86_64-linux-android
- target: x86_64-pc-windows-gnu # only for build os: ubuntu-latest
- target: x86_64-pc-windows-gnu # windows pc
os: ubuntu-latest
- target: x86_64-apple-darwin # Intel mac
os: macos-latest
- target: aarch64-apple-darwin # M chip mac
os: macos-latest
- target: aarch64-unknown-linux-musl # arm64 Linux
os: ubuntu-latest
- target: x86_64-unknown-linux-musl # x86 Linux
os: ubuntu-latest
uses: ./.github/workflows/ksud.yml uses: ./.github/workflows/ksud.yml
with: with:
target: ${{ matrix.target }} target: ${{ matrix.target }}
os: ${{ matrix.os }}

42
.github/workflows/build-lkm.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Build LKM for KernelSU
on:
push:
branches: ["main", "ci", "checkci"]
paths:
- ".github/workflows/gki-kernel.yml"
- ".github/workflows/build-lkm.yml"
- "kernel/**"
pull_request:
branches: ["main"]
paths:
- ".github/workflows/gki-kernel.yml"
- ".github/workflows/build-lkm.yml"
- "kernel/**"
workflow_call:
jobs:
build-lkm:
strategy:
matrix:
include:
- version: "android12-5.10"
sub_level: 198
os_patch_level: "2024-01"
- version: "android13-5.10"
sub_level: 198
os_patch_level: 2023-12
- version: "android13-5.15"
sub_level: 137
os_patch_level: 2023-12
- version: "android14-5.15"
sub_level: 110
os_patch_level: 2023-09
- version: "android14-6.1"
sub_level: 43
os_patch_level: 2023-11
uses: ./.github/workflows/gki-kernel.yml
with:
version: ${{ matrix.version }}
version_name: ${{ matrix.version }}.${{ matrix.sub_level }}
tag: ${{ matrix.version }}-${{ matrix.os_patch_level }}
os_patch_level: ${{ matrix.os_patch_level }}
build_lkm: true

View File

@@ -29,7 +29,7 @@ on:
for example: 2021-11 for example: 2021-11
default: 2022-05 default: 2022-05
patch_path: patch_path:
required: true required: false
type: string type: string
description: > description: >
Directory name of .github/patches/<patch_path> Directory name of .github/patches/<patch_path>
@@ -49,6 +49,10 @@ on:
required: false required: false
type: boolean type: boolean
default: false default: false
build_lkm:
required: false
type: boolean
default: false
secrets: secrets:
BOOT_SIGN_KEY: BOOT_SIGN_KEY:
required: false required: false
@@ -154,6 +158,34 @@ jobs:
max-size: 2G max-size: 2G
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
- name: Setup for LKM
if: ${{ inputs.build_lkm == true }}
working-directory: android-kernel
run: |
pip install ast-grep-cli
sudo apt-get install llvm-15 -y
ast-grep -U -p '$$$ check_exports($$$) {$$$}' -r '' common/scripts/mod/modpost.c
ast-grep -U -p 'check_exports($$$);' -r '' common/scripts/mod/modpost.c
sed -i '1i KSU_MODULE := 1' $GITHUB_WORKSPACE/KernelSU/kernel/Makefile
echo "drivers/kernelsu/kernelsu.ko" >> common/android/gki_aarch64_modules
# bazel build, android14-5.15, android14-6.1 use bazel
if [ ! -e build/build.sh ]; then
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' build/kernel/*.sh || echo "No unknown symbol scripts found"
if [ -e common/modules.bzl ]; then
sed -i 's/_COMMON_GKI_MODULES_LIST = \[/_COMMON_GKI_MODULES_LIST = \[ "drivers\/kernelsu\/kernelsu.ko",/g' common/modules.bzl
fi
else
TARGET_FILE="build/kernel/build.sh"
if [ ! -e "$TARGET_FILE" ]; then
TARGET_FILE="build/build.sh"
fi
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' $TARGET_FILE || echo "No unknown symbol in $TARGET_FILE"
sed -i 's/if ! diff -u "\${KERNEL_DIR}\/\${MODULES_ORDER}" "\${OUT_DIR}\/modules\.order"; then/if false; then/g' $TARGET_FILE
sed -i 's@${ROOT_DIR}/build/abi/compare_to_symbol_list@echo@g' $TARGET_FILE
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' build/kernel/*.sh || echo "No unknown symbol scripts found"
fi
- name: Make working directory clean to avoid dirty - name: Make working directory clean to avoid dirty
working-directory: android-kernel working-directory: android-kernel
run: | run: |
@@ -163,7 +195,7 @@ jobs:
cd common/ && git add -A && git commit -a -m "Add KernelSU" cd common/ && git add -A && git commit -a -m "Add KernelSU"
repo status repo status
- name: Build boot.img - name: Build Kernel/LKM
working-directory: android-kernel working-directory: android-kernel
run: | run: |
if [ ! -z ${{ vars.EXPECTED_SIZE }} ] && [ ! -z ${{ vars.EXPECTED_HASH }} ]; then if [ ! -z ${{ vars.EXPECTED_SIZE }} ] && [ ! -z ${{ vars.EXPECTED_HASH }} ]; then
@@ -184,20 +216,34 @@ jobs:
OUTDIR=android-kernel/dist OUTDIR=android-kernel/dist
fi fi
mkdir output mkdir output
cp $OUTDIR/Image ./output/ if [ "${{ inputs.build_lkm}}" = "true" ]; then
cp $OUTDIR/Image.lz4 ./output/ llvm-strip-15 $OUTDIR/kernelsu.ko
git clone https://github.com/Kernel-SU/AnyKernel3 cp $OUTDIR/kernelsu.ko ./output/
rm -rf ./AnyKernel3/.git else
cp $OUTDIR/Image ./AnyKernel3/ cp $OUTDIR/Image ./output/
cp $OUTDIR/Image.lz4 ./output/
git clone https://github.com/Kernel-SU/AnyKernel3
rm -rf ./AnyKernel3/.git
cp $OUTDIR/Image ./AnyKernel3/
fi
- name: Upload Image and Image.gz - name: Upload Image and Image.gz
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: ${{ inputs.build_lkm == false }}
with: with:
name: Image-${{ inputs.version_name }}_${{ inputs.os_patch_level }} name: Image-${{ inputs.version_name }}_${{ inputs.os_patch_level }}
path: ./output/* path: ./output/*
- name: Upload AnyKernel3 - name: Upload AnyKernel3
if: ${{ inputs.build_lkm == false }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: AnyKernel3-${{ inputs.version_name }}_${{ inputs.os_patch_level }} name: AnyKernel3-${{ inputs.version_name }}_${{ inputs.os_patch_level }}
path: ./AnyKernel3/* path: ./AnyKernel3/*
- name: Upload kernelsu.ko
uses: actions/upload-artifact@v4
if: ${{ inputs.build_lkm == true }}
with:
name: ${{ inputs.version }}_kernelsu.ko
path: ./output/kernelsu.ko

View File

@@ -5,19 +5,27 @@ on:
target: target:
required: true required: true
type: string type: string
os:
required: false
type: string
default: ubuntu-latest
use_cache: use_cache:
required: false required: false
type: boolean type: boolean
default: true default: true
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ${{ inputs.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
# cross build failed after Rust 1.68, see https://github.com/cross-rs/cross/issues/1222 # cross build failed after Rust 1.68, see https://github.com/cross-rs/cross/issues/1222
- run: rustup default 1.67.0 - name: Setup rustup
run: |
rustup default 1.67.0
rustup target add x86_64-apple-darwin
rustup target add aarch64-apple-darwin
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
workspaces: userspace/ksud workspaces: userspace/ksud
@@ -33,4 +41,4 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ksud-${{ inputs.target }} name: ksud-${{ inputs.target }}
path: userspace/ksud/target/**/release/ksud path: userspace/ksud/target/**/release/ksud*

View File

@@ -14,4 +14,11 @@ config KSU_DEBUG
help help
Enable KernelSU debug mode Enable KernelSU debug mode
config KSU_MODULE
bool "Build KernelSU as a module"
depends on KSU
default n
help
Build KernelSU as a loadable kernel module
endmenu endmenu

View File

@@ -1,17 +1,27 @@
obj-y += ksu.o kernelsu-objs := ksu.o
obj-y += allowlist.o kernelsu-objs += allowlist.o
kernelsu-objs := apk_sign.o kernelsu-objs += apk_sign.o
obj-y += kernelsu.o kernelsu-objs += module_api.o
obj-y += module_api.o kernelsu-objs += sucompat.o
obj-y += sucompat.o kernelsu-objs += uid_observer.o
obj-y += uid_observer.o kernelsu-objs += manager.o
obj-y += manager.o kernelsu-objs += core_hook.o
obj-y += core_hook.o kernelsu-objs += ksud.o
obj-y += ksud.o kernelsu-objs += embed_ksud.o
obj-y += embed_ksud.o kernelsu-objs += kernel_compat.o
obj-y += kernel_compat.o
kernelsu-objs += selinux/selinux.o
kernelsu-objs += selinux/sepolicy.o
kernelsu-objs += selinux/rules.o
ccflags-y += -I$(srctree)/security/selinux -I$(srctree)/security/selinux/include
ccflags-y += -I$(objtree)/security/selinux -include $(srctree)/include/uapi/asm-generic/errno.h
ifndef KSU_MODULE
obj-y += kernelsu.o
else
obj-m += kernelsu.o
endif
obj-y += selinux/
# .git is a text file while the module is imported by 'git submodule add'. # .git is a text file while the module is imported by 'git submodule add'.
ifeq ($(shell test -e $(srctree)/$(src)/../.git; echo $$?),0) ifeq ($(shell test -e $(srctree)/$(src)/../.git; echo $$?),0)
$(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin [ -f ../.git/shallow ] && git fetch --unshallow) $(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin [ -f ../.git/shallow ] && git fetch --unshallow)
@@ -25,6 +35,14 @@ $(warning "KSU_GIT_VERSION not defined! It is better to make KernelSU a git subm
ccflags-y += -DKSU_VERSION=16 ccflags-y += -DKSU_VERSION=16
endif endif
ifeq ($(shell grep -q " current_sid(void)" $(srctree)/security/selinux/include/objsec.h; echo $$?),0)
ccflags-y += -DKSU_COMPAT_HAS_CURRENT_SID
endif
ifeq ($(shell grep -q "struct selinux_state " $(srctree)/security/selinux/include/security.h; echo $$?),0)
ccflags-y += -DKSU_COMPAT_HAS_SELINUX_STATE
endif
ifndef KSU_EXPECTED_SIZE ifndef KSU_EXPECTED_SIZE
KSU_EXPECTED_SIZE := 0x033b KSU_EXPECTED_SIZE := 0x033b
endif endif
@@ -43,5 +61,6 @@ $(info -- KernelSU Manager signature hash: $(KSU_EXPECTED_HASH))
ccflags-y += -DEXPECTED_SIZE=$(KSU_EXPECTED_SIZE) ccflags-y += -DEXPECTED_SIZE=$(KSU_EXPECTED_SIZE)
ccflags-y += -DEXPECTED_HASH=\"$(KSU_EXPECTED_HASH)\" ccflags-y += -DEXPECTED_HASH=\"$(KSU_EXPECTED_HASH)\"
ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat
ccflags-y += -Wno-declaration-after-statement ccflags-y += -Wno-declaration-after-statement -Wno-unused-function

View File

@@ -4,12 +4,20 @@
#include "linux/err.h" #include "linux/err.h"
#include "linux/init.h" #include "linux/init.h"
#include "linux/init_task.h" #include "linux/init_task.h"
#include "linux/kallsyms.h"
#include "linux/kernel.h" #include "linux/kernel.h"
#include "linux/kprobes.h" #include "linux/kprobes.h"
#include "linux/list.h"
#include "linux/lsm_hooks.h" #include "linux/lsm_hooks.h"
#include "linux/mm.h"
#include "linux/mm_types.h"
#include "linux/nsproxy.h" #include "linux/nsproxy.h"
#include "linux/path.h" #include "linux/path.h"
#include "linux/printk.h" #include "linux/printk.h"
#include "linux/sched.h"
#include "linux/security.h"
#include "linux/stddef.h"
#include "linux/types.h"
#include "linux/uaccess.h" #include "linux/uaccess.h"
#include "linux/uidgid.h" #include "linux/uidgid.h"
#include "linux/version.h" #include "linux/version.h"
@@ -25,6 +33,7 @@
#include "klog.h" // IWYU pragma: keep #include "klog.h" // IWYU pragma: keep
#include "ksu.h" #include "ksu.h"
#include "ksud.h" #include "ksud.h"
#include "linux/vmalloc.h"
#include "manager.h" #include "manager.h"
#include "selinux/selinux.h" #include "selinux/selinux.h"
#include "uid_observer.h" #include "uid_observer.h"
@@ -236,7 +245,7 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
#ifdef CONFIG_KSU_DEBUG #ifdef CONFIG_KSU_DEBUG
pr_info("manager already exist: %d\n", pr_info("manager already exist: %d\n",
ksu_get_manager_uid()); ksu_get_manager_uid());
#endif #endif
return 0; return 0;
} }
@@ -726,14 +735,181 @@ void __init ksu_lsm_hook_init(void)
#endif #endif
} }
#ifdef MODULE
static int override_security_head(void *head, const void *new_head, size_t len)
{
unsigned long base = (unsigned long)head & PAGE_MASK;
unsigned long offset = offset_in_page(head);
// this is impossible for our case because the page alignment
// but be careful for other cases!
BUG_ON(offset + len > PAGE_SIZE);
struct page *page = phys_to_page(__pa(base));
if (!page) {
return -EFAULT;
}
void *addr = vmap(&page, 1, VM_MAP, PAGE_KERNEL);
if (!addr) {
return -ENOMEM;
}
memcpy(addr + offset, new_head, len);
vunmap(addr);
return 0;
}
static void free_security_hook_list(struct hlist_head *head)
{
struct hlist_node *temp;
struct security_hook_list *entry;
if (!head)
return;
hlist_for_each_entry_safe (entry, temp, head, list) {
hlist_del(&entry->list);
kfree(entry);
}
kfree(head);
}
struct hlist_head *copy_security_hlist(struct hlist_head *orig)
{
struct hlist_head *new_head = kmalloc(sizeof(*new_head), GFP_KERNEL);
if (!new_head)
return NULL;
INIT_HLIST_HEAD(new_head);
struct security_hook_list *entry;
struct security_hook_list *new_entry;
hlist_for_each_entry (entry, orig, list) {
new_entry = kmalloc(sizeof(*new_entry), GFP_KERNEL);
if (!new_entry) {
free_security_hook_list(new_head);
return NULL;
}
*new_entry = *entry;
hlist_add_tail_rcu(&new_entry->list, new_head);
}
return new_head;
}
#define LSM_SEARCH_MAX 180 // This should be enough to iterate
static void *find_head_addr(void *security_ptr, int *index)
{
if (!security_ptr) {
return NULL;
}
struct hlist_head *head_start =
(struct hlist_head *)&security_hook_heads;
for (int i = 0; i < LSM_SEARCH_MAX; i++) {
struct hlist_head *head = head_start + i;
struct security_hook_list *pos;
hlist_for_each_entry (pos, head, list) {
if (pos->hook.capget == security_ptr) {
if (index) {
*index = i;
}
return head;
}
}
}
return NULL;
}
#define GET_SYMBOL_ADDR(sym) \
({ \
void *addr = kallsyms_lookup_name(#sym ".cfi_jt"); \
if (!addr) { \
addr = kallsyms_lookup_name(#sym); \
} \
addr; \
})
#define KSU_LSM_HOOK_HACK_INIT(head_ptr, name, func) \
do { \
static struct security_hook_list hook = { \
.hook = { .name = func } \
}; \
hook.head = head_ptr; \
hook.lsm = "ksu"; \
struct hlist_head *new_head = copy_security_hlist(hook.head); \
if (!new_head) { \
pr_err("Failed to copy security list: %s\n", #name); \
break; \
} \
hlist_add_tail_rcu(&hook.list, new_head); \
if (override_security_head(hook.head, new_head, \
sizeof(*new_head))) { \
free_security_hook_list(new_head); \
pr_err("Failed to hack lsm for: %s\n", #name); \
} \
} while (0)
void __init ksu_lsm_hook_init_hack(void)
{
void *cap_prctl = GET_SYMBOL_ADDR(cap_task_prctl);
void *prctl_head = find_head_addr(cap_prctl, NULL);
if (prctl_head) {
if (prctl_head != &security_hook_heads.task_prctl) {
pr_warn("prctl's address has shifted!\n");
}
KSU_LSM_HOOK_HACK_INIT(prctl_head, task_prctl, ksu_task_prctl);
} else {
pr_warn("Failed to find task_prctl!\n");
}
int inode_killpriv_index = -1;
void *cap_killpriv = GET_SYMBOL_ADDR(cap_inode_killpriv);
find_head_addr(cap_killpriv, &inode_killpriv_index);
if (inode_killpriv_index < 0) {
pr_warn("Failed to find inode_rename, use kprobe instead!\n");
register_kprobe(&renameat_kp);
} else {
int inode_rename_index = inode_killpriv_index +
&security_hook_heads.inode_rename -
&security_hook_heads.inode_killpriv;
struct hlist_head *head_start =
(struct hlist_head *)&security_hook_heads;
void *inode_rename_head = head_start + inode_rename_index;
if (inode_rename_head != &security_hook_heads.inode_rename) {
pr_warn("inode_rename's address has shifted!\n");
}
KSU_LSM_HOOK_HACK_INIT(inode_rename_head, inode_rename,
ksu_inode_rename);
}
void *cap_setuid = GET_SYMBOL_ADDR(cap_task_fix_setuid);
void *setuid_head = find_head_addr(cap_setuid, NULL);
if (setuid_head) {
if (setuid_head != &security_hook_heads.task_fix_setuid) {
pr_warn("setuid's address has shifted!\n");
}
KSU_LSM_HOOK_HACK_INIT(setuid_head, task_fix_setuid,
ksu_task_fix_setuid);
} else {
pr_warn("Failed to find task_fix_setuid!\n");
}
smp_mb();
}
#endif
void __init ksu_core_init(void) void __init ksu_core_init(void)
{ {
#ifndef MODULE #ifndef MODULE
pr_info("ksu_lsm_hook_init\n"); pr_info("ksu_lsm_hook_init\n");
ksu_lsm_hook_init(); ksu_lsm_hook_init();
#else #else
pr_info("ksu_kprobe_init\n"); pr_info("ksu_lsm_hook_init hack!!!!\n");
ksu_kprobe_init(); ksu_lsm_hook_init_hack();
#endif #endif
} }

View File

@@ -37,6 +37,23 @@ fun parseKernelVersion(version: String): KernelVersion {
} }
} }
fun parseKMI(input: String): String? {
val regex = Regex("(.* )?(\\d+\\.\\d+)(\\S+)?(android\\d+)(.*)")
val result = regex.find(input)
return result?.let {
val androidVersion = it.groups[4]?.value ?: ""
val kernelVersion = it.groups[2]?.value ?: ""
"$androidVersion-$kernelVersion"
}
}
fun getKMI(): String? {
Os.uname().release.let {
return parseKMI(it)
}
}
fun getKernelVersion(): KernelVersion { fun getKernelVersion(): KernelVersion {
Os.uname().release.let { Os.uname().release.let {
return parseKernelVersion(it) return parseKernelVersion(it)

View File

@@ -0,0 +1,189 @@
package me.weishu.kernelsu.ui.screen
import android.net.Uri
import android.os.Environment
import android.os.Parcelable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import me.weishu.kernelsu.R
import me.weishu.kernelsu.ui.component.KeyEventBlocker
import me.weishu.kernelsu.ui.util.LocalSnackbarHost
import me.weishu.kernelsu.ui.util.installBoot
import me.weishu.kernelsu.ui.util.installModule
import me.weishu.kernelsu.ui.util.reboot
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
/**
* @author weishu
* @date 2023/1/1.
*/
@OptIn(ExperimentalComposeUiApi::class)
@Composable
@Destination
fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
var text by rememberSaveable { mutableStateOf("") }
val logContent = rememberSaveable { StringBuilder() }
var showFloatAction by rememberSaveable { mutableStateOf(false) }
val snackBarHost = LocalSnackbarHost.current
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
LaunchedEffect(Unit) {
if (text.isNotEmpty()) {
return@LaunchedEffect
}
withContext(Dispatchers.IO) {
flashIt(flashIt, onFinish = { showReboot ->
if (showReboot) {
showFloatAction = true
}
}, onStdout = {
text += "$it\n"
logContent.append(it).append("\n")
}, onStderr = {
logContent.append(it).append("\n")
});
}
}
Scaffold(
topBar = {
TopBar(
onBack = {
navigator.popBackStack()
},
onSave = {
scope.launch {
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
val date = format.format(Date())
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"KernelSU_install_log_${date}.log"
)
file.writeText(logContent.toString())
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
}
}
)
},
floatingActionButton = {
if (showFloatAction) {
val reboot = stringResource(id = R.string.reboot)
ExtendedFloatingActionButton(
onClick = {
scope.launch {
withContext(Dispatchers.IO) {
reboot()
}
}
},
icon = { Icon(Icons.Filled.Refresh, reboot) },
text = { Text(text = reboot) },
)
}
}
) { innerPadding ->
KeyEventBlocker {
it.key == Key.VolumeDown || it.key == Key.VolumeUp
}
Column(
modifier = Modifier
.fillMaxSize(1f)
.padding(innerPadding)
.verticalScroll(scrollState),
) {
LaunchedEffect(text) {
scrollState.animateScrollTo(scrollState.maxValue)
}
Text(
modifier = Modifier.padding(8.dp),
text = text,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontFamily = FontFamily.Monospace,
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
)
}
}
}
@Parcelize
sealed class FlashIt : Parcelable {
data class FlashBoot(val bootUri: Uri? = null, val koUri: Uri, val ota: Boolean) : FlashIt()
data class FlashModule(val uri: Uri) : FlashIt()
}
fun flashIt(
flashIt: FlashIt, onFinish: (Boolean) -> Unit,
onStdout: (String) -> Unit,
onStderr: (String) -> Unit
) {
when (flashIt) {
is FlashIt.FlashBoot -> installBoot(
flashIt.bootUri,
flashIt.koUri,
flashIt.ota,
onFinish,
onStdout,
onStderr
)
is FlashIt.FlashModule -> installModule(flashIt.uri, onFinish, onStdout, onStderr)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
TopAppBar(
title = { Text(stringResource(R.string.install)) },
navigationIcon = {
IconButton(
onClick = onBack
) { Icon(Icons.Filled.ArrowBack, contentDescription = null) }
},
actions = {
IconButton(onClick = onSave) {
Icon(
imageVector = Icons.Filled.Save,
contentDescription = "Localized description"
)
}
}
)
}
@Preview
@Composable
fun InstallPreview() {
// InstallScreen(DestinationsNavigator(), uri = Uri.EMPTY)
}

View File

@@ -34,6 +34,7 @@ import kotlinx.coroutines.withContext
import me.weishu.kernelsu.* import me.weishu.kernelsu.*
import me.weishu.kernelsu.R import me.weishu.kernelsu.R
import me.weishu.kernelsu.ui.component.rememberConfirmDialog import me.weishu.kernelsu.ui.component.rememberConfirmDialog
import me.weishu.kernelsu.ui.screen.destinations.InstallScreenDestination
import me.weishu.kernelsu.ui.screen.destinations.SettingScreenDestination import me.weishu.kernelsu.ui.screen.destinations.SettingScreenDestination
import me.weishu.kernelsu.ui.util.* import me.weishu.kernelsu.ui.util.*
@@ -60,7 +61,9 @@ fun HomeScreen(navigator: DestinationsNavigator) {
} }
val ksuVersion = if (isManager) Natives.version else null val ksuVersion = if (isManager) Natives.version else null
StatusCard(kernelVersion, ksuVersion) StatusCard(kernelVersion, ksuVersion) {
navigator.navigate(InstallScreenDestination)
}
if (isManager && Natives.requireNewKernel()) { if (isManager && Natives.requireNewKernel()) {
WarningCard( WarningCard(
stringResource(id = R.string.require_kernel_version).format( stringResource(id = R.string.require_kernel_version).format(
@@ -68,7 +71,7 @@ fun HomeScreen(navigator: DestinationsNavigator) {
) )
) )
} }
if (!rootAvailable()) { if (ksuVersion != null && !rootAvailable()) {
WarningCard( WarningCard(
stringResource(id = R.string.grant_root_failed) stringResource(id = R.string.grant_root_failed)
) )
@@ -174,7 +177,7 @@ private fun TopBar(onSettingsClick: () -> Unit) {
} }
@Composable @Composable
private fun StatusCard(kernelVersion: KernelVersion, ksuVersion: Int?) { private fun StatusCard(kernelVersion: KernelVersion, ksuVersion: Int?, onClickInstall: () -> Unit = {}) {
ElevatedCard( ElevatedCard(
colors = CardDefaults.elevatedCardColors(containerColor = run { colors = CardDefaults.elevatedCardColors(containerColor = run {
if (ksuVersion != null) MaterialTheme.colorScheme.secondaryContainer if (ksuVersion != null) MaterialTheme.colorScheme.secondaryContainer
@@ -185,8 +188,8 @@ private fun StatusCard(kernelVersion: KernelVersion, ksuVersion: Int?) {
Row(modifier = Modifier Row(modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
if (kernelVersion.isGKI() && ksuVersion == null) { if (kernelVersion.isGKI()) {
uriHandler.openUri("https://kernelsu.org/guide/installation.html") onClickInstall()
} }
} }
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) { .padding(24.dp), verticalAlignment = Alignment.CenterVertically) {

View File

@@ -1,140 +1,262 @@
package me.weishu.kernelsu.ui.screen package me.weishu.kernelsu.ui.screen
import android.app.Activity
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Environment import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.Button
import androidx.compose.material.icons.filled.Save import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.* import androidx.compose.material3.Icon
import androidx.compose.runtime.* import androidx.compose.material3.IconButton
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.input.key.key
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.weishu.kernelsu.R import me.weishu.kernelsu.R
import me.weishu.kernelsu.ui.component.KeyEventBlocker import me.weishu.kernelsu.ui.component.rememberConfirmDialog
import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.component.rememberLoadingDialog
import me.weishu.kernelsu.ui.util.installModule import me.weishu.kernelsu.ui.screen.destinations.FlashScreenDestination
import me.weishu.kernelsu.ui.util.reboot import me.weishu.kernelsu.ui.util.DownloadListener
import java.io.File import me.weishu.kernelsu.ui.util.download
import java.text.SimpleDateFormat import me.weishu.kernelsu.ui.util.getLKMUrl
import java.util.* import me.weishu.kernelsu.ui.util.isAbDevice
import me.weishu.kernelsu.ui.util.rootAvailable
/** /**
* @author weishu * @author weishu
* @date 2023/1/1. * @date 2024/3/12.
*/ */
@OptIn(ExperimentalComposeUiApi::class)
@Composable
@Destination @Destination
fun InstallScreen(navigator: DestinationsNavigator, uri: Uri) { @Composable
fun InstallScreen(navigator: DestinationsNavigator) {
var text by rememberSaveable { mutableStateOf("") }
val logContent = rememberSaveable { StringBuilder() }
var showFloatAction by rememberSaveable { mutableStateOf(false) }
val snackBarHost = LocalSnackbarHost.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val scrollState = rememberScrollState() val loadingDialog = rememberLoadingDialog()
val context = LocalContext.current
var installMethod by remember {
mutableStateOf<InstallMethod?>(null)
}
LaunchedEffect(Unit) { val onFileDownloaded = { uri: Uri ->
if (text.isNotEmpty()) {
return@LaunchedEffect installMethod?.let {
} scope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) { when (it) {
installModule(uri, onFinish = { success -> InstallMethod.DirectInstall -> {
if (success) { navigator.navigate(
showFloatAction = true FlashScreenDestination(
FlashIt.FlashBoot(
null,
uri,
false
)
)
)
}
InstallMethod.DirectInstallToInactiveSlot -> {
navigator.navigate(
FlashScreenDestination(
FlashIt.FlashBoot(
null,
uri,
true
)
)
)
}
is InstallMethod.SelectFile -> {
navigator.navigate(
FlashScreenDestination(
FlashIt.FlashBoot(
it.uri,
uri,
false
)
)
)
}
} }
}, onStdout = { }
text += "$it\n"
logContent.append(it).append("\n")
}, onStderr = {
logContent.append(it).append("\n")
});
} }
} }
Scaffold( Scaffold(topBar = {
topBar = { TopBar {
TopBar( navigator.popBackStack()
onBack = { }
navigator.popBackStack() }) {
}, Column(modifier = Modifier.padding(it)) {
onSave = { SelectInstallMethod { method ->
scope.launch { installMethod = method
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()) }
val date = format.format(Date())
val file = File( Row(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), modifier = Modifier
"KernelSU_install_log_${date}.log" .fillMaxWidth()
) .padding(16.dp)
file.writeText(logContent.toString()) ) {
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
} DownloadListener(context = context) { uri ->
onFileDownloaded(uri)
loadingDialog.hide()
} }
) Button(
}, modifier = Modifier.fillMaxWidth(),
floatingActionButton = { enabled = installMethod != null,
if (showFloatAction) {
val reboot = stringResource(id = R.string.reboot)
ExtendedFloatingActionButton(
onClick = { onClick = {
scope.launch { loadingDialog.showLoading()
withContext(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
reboot() getLKMUrl().onFailure { throwable ->
loadingDialog.hide()
scope.launch(Dispatchers.Main) {
Toast.makeText(
context,
"Failed to fetch LKM url: ${throwable.message}",
Toast.LENGTH_SHORT
).show()
}
}.onSuccess { result ->
loadingDialog.hide()
download(
context = context,
url = result.second,
fileName = result.first,
description = "Downloading ${result.first}",
onDownloaded = { uri ->
onFileDownloaded(uri)
loadingDialog.hide()
},
onDownloading = {}
)
} }
} }
}, }) {
icon = { Icon(Icons.Filled.Refresh, reboot) }, Text("Next", fontSize = MaterialTheme.typography.bodyMedium.fontSize)
text = { Text(text = reboot) }, }
}
}
}
}
sealed class InstallMethod {
data class SelectFile(val uri: Uri? = null, override val label: Int = R.string.select_file) :
InstallMethod()
object DirectInstall : InstallMethod() {
override val label: Int
get() = R.string.direct_install
}
object DirectInstallToInactiveSlot : InstallMethod() {
override val label: Int
get() = R.string.install_inactive_slot
}
abstract val label: Int
}
@Composable
private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
val rootAvailable = rootAvailable()
val isAbDevice = isAbDevice()
val radioOptions = mutableListOf<InstallMethod>(InstallMethod.SelectFile())
if (rootAvailable) {
radioOptions.add(InstallMethod.DirectInstall)
if (isAbDevice) {
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
}
}
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
val selectImageLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri ->
val option = InstallMethod.SelectFile(uri)
selectedOption = option
onSelected(option)
}
}
}
val confirmDialog = rememberConfirmDialog(onConfirm = {
selectedOption = InstallMethod.DirectInstallToInactiveSlot
onSelected(InstallMethod.DirectInstallToInactiveSlot)
}, onDismiss = null)
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)
val onClick = { option: InstallMethod ->
when (option) {
is InstallMethod.SelectFile -> {
selectImageLauncher.launch(
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "application/octet-stream"
}
) )
} }
} is InstallMethod.DirectInstall -> {
) { innerPadding -> selectedOption = option
KeyEventBlocker { onSelected(option)
it.key == Key.VolumeDown || it.key == Key.VolumeUp }
} is InstallMethod.DirectInstallToInactiveSlot -> {
Column( confirmDialog.showConfirm(dialogTitle, dialogContent)
modifier = Modifier }
.fillMaxSize(1f) }
.padding(innerPadding) }
.verticalScroll(scrollState),
) { Column {
LaunchedEffect(text) { radioOptions.forEach { option ->
scrollState.animateScrollTo(scrollState.maxValue) Row(verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
onClick(option)
}) {
RadioButton(selected = option.javaClass == selectedOption?.javaClass, onClick = {
onClick(option)
})
Text(text = stringResource(id = option.label))
} }
Text(
modifier = Modifier.padding(8.dp),
text = text,
fontSize = MaterialTheme.typography.bodySmall.fontSize,
fontFamily = FontFamily.Monospace,
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
)
} }
} }
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) { private fun TopBar(onBack: () -> Unit = {}) {
TopAppBar( TopAppBar(
title = { Text(stringResource(R.string.install)) }, title = { Text(stringResource(R.string.install)) },
navigationIcon = { navigationIcon = {
@@ -142,19 +264,11 @@ private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
onClick = onBack onClick = onBack
) { Icon(Icons.Filled.ArrowBack, contentDescription = null) } ) { Icon(Icons.Filled.ArrowBack, contentDescription = null) }
}, },
actions = {
IconButton(onClick = onSave) {
Icon(
imageVector = Icons.Filled.Save,
contentDescription = "Localized description"
)
}
}
) )
} }
@Preview
@Composable @Composable
fun InstallPreview() { @Preview
// InstallScreen(DestinationsNavigator(), uri = Uri.EMPTY) fun SelectInstall_Preview() {
// InstallScreen(DestinationsNavigator())
} }

View File

@@ -43,7 +43,7 @@ import me.weishu.kernelsu.R
import me.weishu.kernelsu.ui.component.ConfirmResult import me.weishu.kernelsu.ui.component.ConfirmResult
import me.weishu.kernelsu.ui.component.rememberConfirmDialog import me.weishu.kernelsu.ui.component.rememberConfirmDialog
import me.weishu.kernelsu.ui.component.rememberLoadingDialog import me.weishu.kernelsu.ui.component.rememberLoadingDialog
import me.weishu.kernelsu.ui.screen.destinations.InstallScreenDestination import me.weishu.kernelsu.ui.screen.destinations.FlashScreenDestination
import me.weishu.kernelsu.ui.screen.destinations.WebScreenDestination import me.weishu.kernelsu.ui.screen.destinations.WebScreenDestination
import me.weishu.kernelsu.ui.util.* import me.weishu.kernelsu.ui.util.*
import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel
@@ -81,7 +81,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
val data = it.data ?: return@rememberLauncherForActivityResult val data = it.data ?: return@rememberLauncherForActivityResult
val uri = data.data ?: return@rememberLauncherForActivityResult val uri = data.data ?: return@rememberLauncherForActivityResult
navigator.navigate(InstallScreenDestination(uri)) navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri)))
viewModel.markNeedRefresh() viewModel.markNeedRefresh()
@@ -123,7 +123,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
.fillMaxSize(), .fillMaxSize(),
onInstallModule = onInstallModule =
{ {
navigator.navigate(InstallScreenDestination(it)) navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(it)))
}, onClickModule = { id, name, hasWebUi -> }, onClickModule = { id, name, hasWebUi ->
if (hasWebUi) { if (hasWebUi) {
navigator.navigate(WebScreenDestination(id, name)) navigator.navigate(WebScreenDestination(id, name))

View File

@@ -10,6 +10,7 @@ import android.net.Uri
import android.os.Environment import android.os.Environment
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import me.weishu.kernelsu.getKMI
/** /**
* @author weishu * @author weishu
@@ -94,6 +95,38 @@ fun checkNewVersion(): Triple<Int, String, String> {
} }
return defaultValue return defaultValue
} }
fun getLKMUrl(): Result<Pair<String, String>> {
val url = "https://api.github.com/repos/tiann/KernelSU/releases/latest"
val kmi = getKMI() ?: return Result.failure(RuntimeException("Get KMI failed"))
runCatching {
okhttp3.OkHttpClient().newCall(okhttp3.Request.Builder().url(url).build()).execute()
.use { response ->
val body = response.body?.string() ?: return Result.failure(RuntimeException("request body failed"))
if (!response.isSuccessful) {
return Result.failure(RuntimeException("Request failed, code: ${response.code}, message: $body"))
}
val json = org.json.JSONObject(body)
val assets = json.getJSONArray("assets")
for (i in 0 until assets.length()) {
val asset = assets.getJSONObject(i)
val name = asset.getString("name")
if (!name.endsWith(".ko")) {
continue
}
if (name.contains(kmi)) {
return Result.success(Pair(name, asset.getString("browser_download_url")))
}
}
}
}.onFailure {
return Result.failure(it)
}
return Result.failure(RuntimeException("Cannot find LKM for $kmi"))
}
@Composable @Composable
fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) { fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {

View File

@@ -1,6 +1,7 @@
package me.weishu.kernelsu.ui.util package me.weishu.kernelsu.ui.util
import android.net.Uri import android.net.Uri
import android.os.Environment
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.CallbackList
@@ -138,6 +139,84 @@ fun installModule(
} }
} }
fun installBoot(
bootUri: Uri?,
lkmUri: Uri,
ota: Boolean,
onFinish: (Boolean) -> Unit,
onStdout: (String) -> Unit,
onStderr: (String) -> Unit
): Boolean {
val resolver = ksuApp.contentResolver
with(resolver.openInputStream(lkmUri)) {
val lkmFile = File(ksuApp.cacheDir, "kernelsu.ko")
lkmFile.outputStream().use { output ->
this?.copyTo(output)
}
if (!lkmFile.exists()) {
onStdout("- kernelsu.ko not found")
onFinish(false)
return false
}
val bootFile = bootUri?.let { uri ->
with(resolver.openInputStream(uri)) {
val bootFile = File(ksuApp.cacheDir, "boot.img")
bootFile.outputStream().use { output ->
this?.copyTo(output)
}
bootFile
}
}
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so")
var cmd = "boot-patch -m ${lkmFile.absolutePath} --magiskboot ${magiskboot.absolutePath}"
cmd += if (bootFile == null) {
// no boot.img, use -f to force install
" -f"
} else {
" -b ${bootFile.absolutePath}"
}
if (ota) {
cmd += " -u"
}
// output dir
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
cmd += " -o $downloadsDir"
val shell = createRootShell()
val stdoutCallback: CallbackList<String?> = object : CallbackList<String?>() {
override fun onAddElement(s: String?) {
onStdout(s ?: "")
}
}
val stderrCallback: CallbackList<String?> = object : CallbackList<String?>() {
override fun onAddElement(s: String?) {
onStderr(s ?: "")
}
}
val result =
shell.newJob().add("${getKsuDaemonPath()} $cmd").to(stdoutCallback, stderrCallback)
.exec()
Log.i("KernelSU", "install boot $lkmUri result: $result")
lkmFile.delete()
bootFile?.delete()
onFinish(bootUri != null && result.isSuccess)
return result.isSuccess
}
}
fun reboot(reason: String = "") { fun reboot(reason: String = "") {
val shell = getRootShell() val shell = getRootShell()
if (reason == "recovery") { if (reason == "recovery") {
@@ -152,6 +231,11 @@ fun rootAvailable(): Boolean {
return shell.isRoot return shell.isRoot
} }
fun isAbDevice(): Boolean {
val shell = getRootShell()
return ShellUtils.fastCmd(shell, "getprop ro.build.ab_update").trim().toBoolean()
}
fun overlayFsAvailable(): Boolean { fun overlayFsAvailable(): Boolean {
val shell = getRootShell() val shell = getRootShell()
// check /proc/filesystems // check /proc/filesystems

View File

@@ -108,4 +108,8 @@
<string name="open">打开</string> <string name="open">打开</string>
<string name="enable_web_debugging">启用 WebView 调试</string> <string name="enable_web_debugging">启用 WebView 调试</string>
<string name="enable_web_debugging_summary">可用于调试 WebUI ,请仅在需要时启用。</string> <string name="enable_web_debugging_summary">可用于调试 WebUI ,请仅在需要时启用。</string>
<string name="direct_install">直接安装(推荐)</string>
<string name="select_file">选择一个文件</string>
<string name="install_inactive_slot">安装到未使用的槽位OTA 后)</string>
<string name="install_inactive_slot_warning">将在重启后强制切换到另一个槽位!\n注意只能在 OTA 更新完成后的重启之前使用。\n确认</string>
</resources> </resources>

View File

@@ -110,4 +110,8 @@
<string name="open">Open</string> <string name="open">Open</string>
<string name="enable_web_debugging">Enable WebView Debugging</string> <string name="enable_web_debugging">Enable WebView Debugging</string>
<string name="enable_web_debugging_summary">Can be used to debug WebUI, please enable only when needed.</string> <string name="enable_web_debugging_summary">Can be used to debug WebUI, please enable only when needed.</string>
<string name="direct_install">Direct Install (Recommended)</string>
<string name="select_file">Select a File</string>
<string name="install_inactive_slot">Install to Inactive Slot (After OTA)</string>
<string name="install_inactive_slot_warning">Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue?</string>
</resources> </resources>

Binary file not shown.

Binary file not shown.

BIN
userspace/ksud/bin/x86_64/ksuinit Executable file

Binary file not shown.

View File

@@ -1,29 +1,38 @@
use anyhow::Result; use anyhow::Result;
use const_format::concatcp; use const_format::concatcp;
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use std::path::Path;
use crate::{defs::BINARY_DIR, utils}; use crate::{defs::BINARY_DIR, utils};
pub const RESETPROP_PATH: &str = concatcp!(BINARY_DIR, "resetprop"); pub const RESETPROP_PATH: &str = concatcp!(BINARY_DIR, "resetprop");
pub const BUSYBOX_PATH: &str = concatcp!(BINARY_DIR, "busybox"); pub const BUSYBOX_PATH: &str = concatcp!(BINARY_DIR, "busybox");
pub const BOOTCTL_PATH: &str = concatcp!(BINARY_DIR, "bootctl");
#[cfg(target_arch = "aarch64")] #[cfg(all(target_arch = "x86_64", target_os = "android"))]
#[derive(RustEmbed)]
#[folder = "bin/aarch64"]
struct Asset;
#[cfg(target_arch = "x86_64")]
#[derive(RustEmbed)] #[derive(RustEmbed)]
#[folder = "bin/x86_64"] #[folder = "bin/x86_64"]
struct Asset; struct Asset;
// IF NOT x86_64 ANDROID, ie. macos, linux, windows, always use aarch64
#[cfg(not(all(target_arch = "x86_64", target_os = "android")))]
#[derive(RustEmbed)]
#[folder = "bin/aarch64"]
struct Asset;
pub fn ensure_binaries(ignore_if_exist: bool) -> Result<()> { pub fn ensure_binaries(ignore_if_exist: bool) -> Result<()> {
for file in Asset::iter() { for file in Asset::iter() {
utils::ensure_binary( if file == "ksuinit" {
format!("{BINARY_DIR}{file}"), continue;
&Asset::get(&file).unwrap().data, }
ignore_if_exist, let asset = Asset::get(&file).ok_or(anyhow::anyhow!("asset not found: {}", file))?;
)? utils::ensure_binary(format!("{BINARY_DIR}{file}"), &asset.data, ignore_if_exist)?
} }
Ok(()) Ok(())
} }
pub fn copy_assets_to_file(name: &str, dst: impl AsRef<Path>) -> Result<()> {
let asset = Asset::get(name).ok_or(anyhow::anyhow!("asset not found: {}", name))?;
std::fs::write(dst, asset.data)?;
Ok(())
}

View File

@@ -1,27 +1,52 @@
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use anyhow::anyhow;
use anyhow::bail; use anyhow::bail;
use anyhow::ensure; use anyhow::ensure;
use anyhow::Context; use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use is_executable::IsExecutable;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use std::process::Stdio; use std::process::Stdio;
use which::which;
use crate::utils; use crate::{assets, utils};
#[cfg(unix)] #[cfg(target_os = "android")]
fn ensure_gki_kernel() -> Result<()> { fn ensure_gki_kernel() -> Result<()> {
let version = let version = get_kernel_version()?;
procfs::sys::kernel::Version::current().with_context(|| "get kernel version failed")?; let is_gki = version.0 == 5 && version.1 >= 10 || version.2 > 5;
let is_gki = version.major == 5 && version.minor >= 10 || version.major > 5;
ensure!(is_gki, "only support GKI kernel"); ensure!(is_gki, "only support GKI kernel");
Ok(()) Ok(())
} }
#[cfg(target_os = "android")]
pub fn get_kernel_version() -> Result<(i32, i32, i32)> {
use regex::Regex;
let uname = rustix::system::uname();
let version = uname.release().to_string_lossy();
let re = Regex::new(r"(\d+)\.(\d+)\.(\d+)")?;
if let Some(captures) = re.captures(&version) {
let major = captures
.get(1)
.and_then(|m| m.as_str().parse::<i32>().ok())
.ok_or_else(|| anyhow!("Major version parse error"))?;
let minor = captures
.get(2)
.and_then(|m| m.as_str().parse::<i32>().ok())
.ok_or_else(|| anyhow!("Minor version parse error"))?;
let patch = captures
.get(3)
.and_then(|m| m.as_str().parse::<i32>().ok())
.ok_or_else(|| anyhow!("Patch version parse error"))?;
Ok((major, minor, patch))
} else {
Err(anyhow!("Invalid kernel version string"))
}
}
fn do_cpio_cmd(magiskboot: &Path, workding_dir: &Path, cmd: &str) -> Result<()> { fn do_cpio_cmd(magiskboot: &Path, workding_dir: &Path, cmd: &str) -> Result<()> {
let status = Command::new(magiskboot) let status = Command::new(magiskboot)
.current_dir(workding_dir) .current_dir(workding_dir)
@@ -63,8 +88,28 @@ pub fn patch(
out: Option<PathBuf>, out: Option<PathBuf>,
magiskboot_path: Option<PathBuf>, magiskboot_path: Option<PathBuf>,
) -> Result<()> { ) -> Result<()> {
let result = do_patch(image, kernel, kmod, init, ota, flash, out, magiskboot_path);
if let Err(ref e) = result {
println!("- Install Error: {e}");
}
result
}
#[allow(clippy::too_many_arguments)]
fn do_patch(
image: Option<PathBuf>,
kernel: Option<PathBuf>,
kmod: Option<PathBuf>,
init: Option<PathBuf>,
ota: bool,
flash: bool,
out: Option<PathBuf>,
magiskboot_path: Option<PathBuf>,
) -> Result<()> {
println!(include_str!("banner"));
if image.is_none() { if image.is_none() {
#[cfg(unix)] #[cfg(target_os = "android")]
ensure_gki_kernel()?; ensure_gki_kernel()?;
} }
@@ -76,19 +121,17 @@ pub fn patch(
"init and module must not be specified." "init and module must not be specified."
); );
} else { } else {
ensure!( ensure!(kmod.is_some(), "module must be specified");
init.is_some() && kmod.is_some(),
"init and module must be specified"
);
} }
let workding_dir = tempdir::TempDir::new("KernelSU")?; let workding_dir =
tempdir::TempDir::new("KernelSU").with_context(|| "create temp dir failed")?;
let bootimage; let bootimage;
let mut bootdevice = None; let mut bootdevice = None;
if let Some(image) = image { if let Some(ref image) = image {
ensure!(image.exists(), "boot image not found"); ensure!(image.exists(), "boot image not found");
bootimage = std::fs::canonicalize(image)?; bootimage = std::fs::canonicalize(image)?;
} else { } else {
@@ -111,7 +154,7 @@ pub fn patch(
format!("/dev/block/by-name/boot{slot_suffix}") format!("/dev/block/by-name/boot{slot_suffix}")
}; };
println!("bootdevice: {boot_partition}"); println!("- Bootdevice: {boot_partition}");
let tmp_boot_path = workding_dir.path().join("boot.img"); let tmp_boot_path = workding_dir.path().join("boot.img");
dd(&boot_partition, &tmp_boot_path)?; dd(&boot_partition, &tmp_boot_path)?;
@@ -122,37 +165,56 @@ pub fn patch(
bootdevice = Some(boot_partition); bootdevice = Some(boot_partition);
}; };
println!("boot image: {bootimage:?}"); // try extract magiskboot/bootctl
let _ = assets::ensure_binaries(false);
let magiskboot = magiskboot_path // extract magiskboot
.map(std::fs::canonicalize) let magiskboot = {
.transpose()? if which("magiskboot").is_ok() {
.unwrap_or_else(|| "magiskboot".into()); let _ = assets::ensure_binaries(true);
"magiskboot".into()
if !magiskboot.is_executable() { } else {
#[cfg(unix)] // magiskboot is not in $PATH, use builtin or specified one
std::fs::set_permissions(&magiskboot, std::fs::Permissions::from_mode(0o755)) let magiskboot = if let Some(magiskboot_path) = magiskboot_path {
.with_context(|| "set magiskboot executable failed".to_string())?; std::fs::canonicalize(magiskboot_path)?
} } else {
let magiskboot_path = workding_dir.path().join("magiskboot");
ensure!(magiskboot.exists(), "magiskboot not found"); assets::copy_assets_to_file("magiskboot", &magiskboot_path)
.with_context(|| "copy magiskboot failed")?;
magiskboot_path
};
ensure!(magiskboot.exists(), "{magiskboot:?} is not exist");
#[cfg(unix)]
let _ = std::fs::set_permissions(&magiskboot, std::fs::Permissions::from_mode(0o755));
magiskboot
}
};
if let Some(kernel) = kernel { if let Some(kernel) = kernel {
std::fs::copy(kernel, workding_dir.path().join("kernel")) std::fs::copy(kernel, workding_dir.path().join("kernel"))
.with_context(|| "copy kernel from failed".to_string())?; .with_context(|| "copy kernel from failed".to_string())?;
} }
if let (Some(kmod), Some(init)) = (kmod, init) { if let Some(kmod) = kmod {
println!("- Preparing assets");
std::fs::copy(kmod, workding_dir.path().join("kernelsu.ko")) std::fs::copy(kmod, workding_dir.path().join("kernelsu.ko"))
.with_context(|| "copy kernel module failed".to_string())?; .with_context(|| "copy kernel module failed".to_string())?;
std::fs::copy(init, workding_dir.path().join("init")) let init_file = workding_dir.path().join("init");
.with_context(|| "copy init failed".to_string())?; if let Some(init) = init {
std::fs::copy(init, workding_dir.path().join("init"))
.with_context(|| "copy init failed".to_string())?;
} else {
crate::assets::copy_assets_to_file("ksuinit", init_file)
.with_context(|| "copy ksuinit failed")?;
}
// magiskboot unpack boot.img // magiskboot unpack boot.img
// magiskboot cpio ramdisk.cpio 'cp init init.real' // magiskboot cpio ramdisk.cpio 'cp init init.real'
// magiskboot cpio ramdisk.cpio 'add 0755 ksuinit init' // magiskboot cpio ramdisk.cpio 'add 0755 ksuinit init'
// magiskboot cpio ramdisk.cpio 'add 0755 <kmod> kernelsu.ko' // magiskboot cpio ramdisk.cpio 'add 0755 <kmod> kernelsu.ko'
println!("- Unpacking boot image");
let status = Command::new(&magiskboot) let status = Command::new(&magiskboot)
.current_dir(workding_dir.path()) .current_dir(workding_dir.path())
.stdout(Stdio::null()) .stdout(Stdio::null())
@@ -162,6 +224,10 @@ pub fn patch(
.status()?; .status()?;
ensure!(status.success(), "magiskboot unpack failed"); ensure!(status.success(), "magiskboot unpack failed");
let not_magisk = do_cpio_cmd(&magiskboot, workding_dir.path(), "test").is_ok();
ensure!(not_magisk, "Cannot work with Magisk patched image");
println!("- Adding KernelSU LKM");
let is_kernelsu_patched = let is_kernelsu_patched =
do_cpio_cmd(&magiskboot, workding_dir.path(), "exists kernelsu.ko").is_ok(); do_cpio_cmd(&magiskboot, workding_dir.path(), "exists kernelsu.ko").is_ok();
if !is_kernelsu_patched { if !is_kernelsu_patched {
@@ -180,6 +246,7 @@ pub fn patch(
)?; )?;
} }
println!("- Repacking boot image");
// magiskboot repack boot.img // magiskboot repack boot.img
let status = Command::new(&magiskboot) let status = Command::new(&magiskboot)
.current_dir(workding_dir.path()) .current_dir(workding_dir.path())
@@ -189,18 +256,25 @@ pub fn patch(
.arg(bootimage.display().to_string()) .arg(bootimage.display().to_string())
.status()?; .status()?;
ensure!(status.success(), "magiskboot repack failed"); ensure!(status.success(), "magiskboot repack failed");
let new_boot = workding_dir.path().join("new-boot.img");
let out = out.unwrap_or(std::env::current_dir()?); if image.is_some() {
// if image is specified, write to output file
let output_dir = out.unwrap_or(std::env::current_dir()?);
let now = chrono::Utc::now();
let output_image =
output_dir.join(format!("kernelsu_boot_{}.img", now.format("%Y%m%d_%H%M%S")));
let now = chrono::Utc::now(); if std::fs::rename(&new_boot, &output_image).is_err() {
let output_image = out.join(format!( std::fs::copy(&new_boot, &output_image)
"kernelsu_patched_boot_{}.img", .with_context(|| "copy out new boot failed".to_string())?;
now.format("%Y%m%d_%H%M%S") }
)); println!("- Output file is written to");
std::fs::copy(workding_dir.path().join("new-boot.img"), &output_image) println!("- {}", output_image.display().to_string().trim_matches('"'));
.with_context(|| "copy out new boot failed".to_string())?; }
if flash { if flash {
println!("- Flashing new boot image");
let Some(bootdevice) = bootdevice else { let Some(bootdevice) = bootdevice else {
bail!("boot device not found") bail!("boot device not found")
}; };
@@ -210,7 +284,52 @@ pub fn patch(
.status()?; .status()?;
ensure!(status.success(), "set boot device rw failed"); ensure!(status.success(), "set boot device rw failed");
dd(&output_image, &bootdevice).with_context(|| "flash boot failed")?; dd(&new_boot, &bootdevice).with_context(|| "flash boot failed")?;
if ota {
post_ota()?;
}
} }
println!("- Done!");
Ok(())
}
fn post_ota() -> Result<()> {
use crate::defs::ADB_DIR;
use assets::BOOTCTL_PATH;
let status = Command::new(BOOTCTL_PATH).arg("hal-info").status()?;
if !status.success() {
return Ok(());
}
let current_slot = Command::new(BOOTCTL_PATH)
.arg("get-current-slot")
.output()?
.stdout;
let current_slot = String::from_utf8(current_slot)?;
let current_slot = current_slot.trim();
let target_slot = if current_slot == "0" { 1 } else { 0 };
Command::new(BOOTCTL_PATH)
.arg(format!("set-active-boot-slot {target_slot}"))
.status()?;
let post_ota_sh = std::path::Path::new(ADB_DIR)
.join("post-fs-data.d")
.join("post_ota.sh");
let sh_content = format!(
r###"
{BOOTCTL_PATH} mark-boot-successful
rm -f {BOOTCTL_PATH}
rm -f /data/adb/post-fs-data.d/post_ota.sh
"###
);
std::fs::write(&post_ota_sh, sh_content)?;
#[cfg(unix)]
std::fs::set_permissions(post_ota_sh, std::fs::Permissions::from_mode(0o755))?;
Ok(()) Ok(())
} }

View File

@@ -7,7 +7,7 @@ use android_logger::Config;
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
use log::LevelFilter; use log::LevelFilter;
use crate::{apk_sign, debug, defs, init_event, ksucalls, module, utils}; use crate::{apk_sign, assets, debug, defs, init_event, ksucalls, module, utils};
/// KernelSU userspace cli /// KernelSU userspace cli
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -60,10 +60,10 @@ enum Commands {
kernel: Option<PathBuf>, kernel: Option<PathBuf>,
/// LKM module path to replace /// LKM module path to replace
#[arg(short, long, requires("init"))] #[arg(short, long)]
module: Option<PathBuf>, module: Option<PathBuf>,
/// init to be replaced, if use LKM, this must be specified /// init to be replaced
#[arg(short, long, requires("module"))] #[arg(short, long, requires("module"))]
init: Option<PathBuf>, init: Option<PathBuf>,
@@ -304,7 +304,7 @@ pub fn run() -> Result<()> {
utils::copy_sparse_file(src, dst, punch_hole)?; utils::copy_sparse_file(src, dst, punch_hole)?;
Ok(()) Ok(())
} }
Debug::Test => todo!(), Debug::Test => assets::ensure_binaries(false),
}, },
Commands::BootPatch { Commands::BootPatch {

View File

@@ -79,12 +79,12 @@ fn set_identity(uid: u32, gid: u32, groups: &[u32]) {
} }
} }
#[cfg(not(unix))] #[cfg(not(any(target_os = "linux", target_os = "android")))]
pub fn root_shell() -> Result<()> { pub fn root_shell() -> Result<()> {
unimplemented!() unimplemented!()
} }
#[cfg(unix)] #[cfg(any(target_os = "linux", target_os = "android"))]
pub fn root_shell() -> Result<()> { pub fn root_shell() -> Result<()> {
// we are root now, this was set in kernel! // we are root now, this was set in kernel!