Skip to content

tripock/AOSKP

Repository files navigation

AOSKP - Android Open Source Kernel Patch

Next-Gen Root Framework
Inverted logic root manager: su is invisible by default,
access is granted only to authorized applications.


Table of Contents

  1. What is AOSK
  2. Core Principles
  3. Architecture
  4. Components
  5. File-based IPC
  6. Mount Namespace Visibility
  7. Minfilter - Detector Evasion
  8. SELinux and System Properties
  9. Daemon Autostart
  10. Boot/init_boot Patching
  11. Ramdisk Format
  12. Request Protocol
  13. Binary Response Format
  14. Authorization
  15. Grant Storage
  16. Process Discovery
  17. Resolved Issues
  18. Building from Source
  19. Installation
  20. Usage
  21. Project Structure
  22. Security
  23. Compatibility
  24. Comparison with Magisk / KernelSU
  25. Known Limitations
  26. Roadmap
  27. License

What is AOSK

AOSK (Android Open Source Kernel Patch) is a root access management system for Android with a fundamentally different security model: by default, su is completely invisible to all applications. No application can detect the presence of root until the user explicitly grants it permission.

Unlike Magisk (which hides root from selected applications) and KernelSU (which operates at the kernel level), AOSK applies inverted logic: root exists, but nobody can see it until the user allows it. This is a fundamentally safer approach.

The Problem with Existing Solutions

Magisk and similar tools operate on a "root is visible to everyone except the deny-list" model. This means:

  • New applications automatically see root
  • If you forget to add an app to the deny-list - it immediately detects root
  • Banking apps detect Magisk through dozens of signals (files, properties, mount points)
  • The "deny-list" model requires manual management of every application

The AOSK Solution

AOSK flips the model:

  • Root is invisible to all applications by default
  • The user explicitly grants root to selected applications
  • No files, sockets, or ports that detectors can find
  • IPC via ordinary files in /sdcard/ and /data/local/tmp/ - indistinguishable from normal activity
  • Mount namespace isolation: granted applications see /system/xbin/su, everyone else sees nothing

Core Principles

Principle Description
Default-deny su is invisible to everyone. Access only through explicit grant.
File-based IPC No sockets, no ports, no binders. Communication via queue files.
Mount namespace isolation Each app in its own mount namespace. su is mounted only in granted apps' namespaces.
Minimal footprint No files in /system/, no suspicious properties, no processes with known names.
SELinux tolerant Daemon runs with setenforce 0 and spoofs AOSP properties.
Universality Works on AOSP, GSI, stock firmware, Samsung, and more.
Ramdisk autostart Patcher injects init.rc + startup script into the boot image ramdisk.

Architecture

┌─────────────────────────────────────────────────────────┐
│                    Android System                       │
│                                                         │
│  ┌──────────┐     ┌──────────┐     ┌──────────┐       │
│  │ Bank App │     │ Terminal │     │ Game App │       │
│  │ (denied) │     │(granted) │     │ (denied) │       │
│  └────┬─────┘     └────┬─────┘     └──────────┘       │
│       │                │                               │
│       │ no su          │ /system/xbin/su mounted        │
│       ▼                ▼                               │
│  ┌─────────────────────────────────────────┐           │
│  │           Mount Namespace               │           │
│  │  Bank: /system/xbin/ = empty            │           │
│  │  Terminal: /system/xbin/su → aoskd      │           │
│  └─────────────────────────────────────────┘           │
│       │                │                               │
│       │    ┌───────────┘                               │
│       │    │ File IPC: /sdcard/AOSKP/aosk_queue/      │
│       ▼    ▼                                            │
│  ┌─────────────────┐                                   │
│  │     aoskd       │  ← root daemon                    │
│  │  (pid watcher)  │                                   │
│  │  (ns watcher)   │                                   │
│  │  (grant mgr)    │                                   │
│  │  (minfilter)    │                                   │
│  └─────────────────┘                                   │
│       │                                                │
│       ▼                                                │
│  sh -c <command>  (root shell execution)               │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│              Patch Pipeline (PC / Device)                │
│                                                         │
│  boot/init_boot.img                                     │
│       │                                                 │
│       ▼                                                 │
│  aosk_patcher                                           │
│  ├─ Format detection: LZ4 legacy / gzip / raw CPIO     │
│  ├─ Ramdisk decompression                               │
│  ├─ CPIO parsing                                        │
│  ├─ Injection: aoskd.rc + aoskd_start.sh + init.rc     │
│  ├─ Recompression (LZ4 HC level 12 / gzip best)        │
│  └─ Assembly: header + kernel + ramdisk + rest          │
│       │                                                 │
│       ▼                                                 │
│  Flash: dd if=patched.img of=/dev/block/by-name/...     │
└─────────────────────────────────────────────────────────┘

Components

aoskd - daemon

The root access daemon, running with UID 0. The only process with root privileges that accepts requests from clients.

Source code: aoskd/src/main.rs (~806 lines)

Functionality:

  1. Request handling - monitoring two queue directories every 100 ms
  2. Authorization - UID verification via /proc/PID/status and file stat()
  3. Command execution - sh -c <cmd> capturing stdout/stderr/exit code
  4. Grant management - grant/revoke per-package with persistence
  5. Mount namespace watcher - monitoring /proc every 2 sec, mounting/unmounting su in granted apps' namespaces
  6. Minfilter - bind-mounting an empty file over suspicious folders in the target app's namespace
  7. Property spoofing - setting AOSP-compatible values like ro.build.type=user, ro.secure=1, etc.
  8. SELinux - setenforce 0 on startup
  9. Binary installation - copying itself to /data/local/tmp/aoskd and creating a hardlink /data/local/tmp/su

Daemon lifecycle:

daemon_main()
  ├─ setenforce 0
  ├─ spoof_aosp_props()          → ro.build.type=user, ro.secure=1, etc.
  ├─ setup_local()               → cp self → /data/local/tmp/aoskd, ln → su
  ├─ mkdir QUEUE_DIR + APP_QUEUE_DIR
  ├─ Clean up stale queue files
  ├─ load_grants()               → read /data/local/tmp/aosk_grants
  ├─ thread: watcher()           → poll queues every 100ms
  ├─ thread: namespace_watcher() → poll /proc every 2sec
  └─ loop { sleep(1s) }          → main thread waits

su - client

The same binary as aoskd. It determines the operating mode by argv[0]: if the name is su or starts with su - client mode, otherwise - daemon.

Source code: aoskd/src/main.rs (function su_main(), lines 562-614)

Behavior:

  1. su -c 'command' - send command to daemon, print result, exit with return code
  2. su 'command' - same without -c
  3. su without arguments - interactive mode: read commands from stdin, send one at a time
  4. exit - exit interactive mode
  5. ensure_daemon() - if daemon is not running, automatically starts it from /data/local/tmp/aoskd or /sdcard/AOSKP/aoskd

Request format:

pid:<client_PID>
su_cmd:<command>

The pid: line allows the daemon to determine the real UID via /proc/PID/status.

patcher - boot patcher

The boot/init_boot image patcher for Android. Modifies the ramdisk to autostart aoskd on device boot.

Source code: patcher/src/main.rs (~640 lines)

Supported ramdisk formats:

Format Magic Detection Decompression Compression
LZ4 legacy 02 21 4C 18 First 4 bytes lz4::block::decompress LZ4 HC level 12
gzip 1F 8B First 2 bytes flate2::GzDecoder gzip best
raw CPIO 070701/070702 Scan first 4096 bytes - -
unknown - - - -

Patching stages:

[1/6] Detect ramdisk format
[2/6] Parse CPIO archive
[3/6] Inject files:
      + aoskd.rc     (init service definition)
      + aoskd_start.sh (startup script)
[4/6] Patch init.rc:
      - If init.rc exists → add "import /aoskd.rc"
      - If init.rc doesn't exist → create minimal init.rc with early-init exec
      - GKI ramdisk: first-stage init doesn't parse .rc from root,
        but aoskd.rc is picked up via import
[5/6] Recompress + assemble CPIO
[6/6] Final size (truncate to original via set_len)

Injected files:

aoskd.rc - init service definition:

service aoskd /aoskd_start.sh
    class late_start
    user root
    group root system
    oneshot
    seclabel u:r:su:s0

aoskd_start.sh - startup script:

#!/system/bin/sh
sleep 10
cp /sdcard/AOSKP/aoskd /data/local/tmp/aoskd
chmod 755 /data/local/tmp/aoskd
mkdir -p /data/local/tmp/aosk_queue
chmod 777 /data/local/tmp/aosk_queue
mkdir -p /sdcard/AOSKP/aosk_queue
/data/local/tmp/aoskd

The sleep 10 delay gives the system time to fully boot and mount /sdcard/, where the aoskd binary is stored.

Output file truncation:

The patcher truncates the output file to the original size via out_file.set_len(input_size). This is necessary because:

  • The boot partition has a fixed size
  • Injection adds data, but recompression (especially LZ4 HC level 12) can reduce the size
  • Typical delta after injection is ~208 bytes (LZ4 HC compresses well)
  • The bootloader requires exact match to partition size

aosk-gui - desktop GUI

Cross-platform desktop application built with Rust + iced 0.13 for managing AOSK via ADB.

Source code: aosk-gui/src/main.rs (~637 lines)

Functionality:

  1. Device IP input - for ADB over Wi-Fi connection (empty field = USB)
  2. Daemon status check - pidof aoskd via ADB
  3. Daemon launch - copy binary + nohup start
  4. Full patch pipeline:
    • adb root + disable-verity + remount
    • Boot/init_boot partition search
    • Boot image backup on device
    • adb pull → patch on PC → adb pushdd flash
    • Fallback: patch on device if PC patcher is unavailable
  5. Patcher log - scrollable area with patcher output
  6. Backup location selection - /sdcard/AOSKP, /data/local/tmp, /system_ext, /vendor

Dark theme:

Background: #12121A
Primary:   #64F58A (green)
Danger:    #FF6464 (red)
Cards:     #1A1A2E with border #333344

Patch pipeline (detailed):

1. adb root
2. disable-verity (if needed → reboot → wait)
3. remount
4. mkdir on device
5. Copy aoskd + aosk_patcher binaries to /data/local/tmp/
6. Find boot partition:
   /dev/block/by-name/init_boot_a
   /dev/block/by-name/init_boot
   /dev/block/by-name/boot_a
   /dev/block/by-name/boot
7. dd if=boot_part of=backup.img
8. adb pull backup.img → PC
9. Run aosk_patcher on PC
   (fallback: patch on device)
10. adb push patched.img → device
11. dd if=patched.img of=boot_part
12. Start aoskd

Android application

Native Android application for managing AOSK directly on the device.

Source code: app/src/main/java/com/aoskpatch/

Interface tabs:

Tab Description
Minfilter Evasion configuration: target apps, hidden folders, hidden packages
Install Daemon status, daemon launch, backup location, patch button
Apps List of all installed apps with grant/revoke switches

Key classes:

MainActivity.kt - main activity:

  • BottomNavigationView with 3 tabs
  • Minfilter configuration with SharedPreferences
  • Patch pipeline via RootBridge
  • RecyclerView with app icons
  • Auto-extraction of binaries from assets → /sdcard/AOSKP/
  • App search

RootBridge.kt - root communication bridge:

  • File-based IPC via /sdcard/AOSKP/aosk_queue/ and /data/local/tmp/aosk_queue/
  • Request format: pid:<PID>\n<message>
  • Binary response format: [stdout+stderr][4 bytes LE exit code]
  • Fallback: direct su process if daemon is not running
  • ADB execution via AdbBridge
  • Grant reading from file or daemon

AdbBridge.kt - ADB connection (wireless):

  • dadb library (pure Java ADB client)
  • Auto-generated ADB key pair
  • 30-second connection timeout

BootReceiver.kt - autostart after reboot:

  • BOOT_COMPLETED → execute su -c 'echo boot'
  • This triggers ensure_daemon() in the su client
  • If aoskd is not running, it will be automatically started

File-based IPC

AOSK uses file-based IPC instead of traditional Android mechanisms (binder, unix sockets, etc.). This is a key architectural decision for undetectability.

Why not sockets?

Mechanism Problem
Abstract unix socket (\0aosk_root_socket) SELinux blocks untrusted_appsu
TCP/UDP ports Detectors scan ports
Binder Requires system registration, visible in service list
Named unix socket File in filesystem, discoverable

How file-based IPC works

Queue directories:

  1. /data/local/tmp/aosk_queue/ - primary queue (for root/shell clients)
  2. /sdcard/AOSKP/aosk_queue/ - app queue (for untrusted_app via sdcard)

Process:

Client                           Daemon
  │                                │
  ├─ Create file req_<pid>_<ts>   │
  │  Contents:                     │
  │  pid:12345                     │
  │  su_cmd:id                     │
  │                                │
  │◄──────── poll every 100ms ────┤
  │                                ├─ Read req file
  │                                ├─ Determine UID (pid→/proc→Uid)
  │                                ├─ Check authorization
  │                                ├─ Execute sh -c <cmd>
  │                                ├─ Write req_<pid>_<ts>.resp
  │                                │  [stdout+stderr][exit_code LE i32]
  │                                ├─ Delete req file
  │                                │
  ├─ Wait for .resp file           │
  │  (poll every 50ms,            │
  │   timeout 10 sec)             │
  │                                │
  ├─ Read .resp                    │
  ├─ Delete .resp                  │
  │                                │
  ▼                                ▼
 Result                           Next request

Why this is undetectable:

  1. Files in /sdcard/AOSKP/aosk_queue/ look like normal app activity
  2. No network connections, no sockets, no open ports
  3. Files are created and deleted quickly (hundreds of milliseconds)
  4. Even if a detector scans /sdcard/, files are already deleted by check time
  5. /data/local/tmp/aosk_queue/ is inaccessible to untrusted_app (shell/root only)

UID Resolution

Critical problem: sdcardfs maps UIDs. A file created by an app with UID 10XXX on /sdcard/ shows a fake UID (e.g. 10539) instead of the real one. stat() returns a fake UID.

Solution: two-level verification

fn resolve_uid(req_path: &str) -> Option<u32> {
    // Priority 1: pid: line in request → /proc/PID/status
    if let Ok(content) = fs::read_to_string(req_path) {
        for line in content.lines() {
            if line.starts_with("pid:") {
                if let Ok(pid) = line[4..].parse::<i32>() {
                    if let Some(uid) = get_uid_of_pid(pid) {
                        return Some(uid);  // Real UID from /proc
                    }
                }
            }
        }
    }
    // Priority 2: stat() UID (for root and shell)
    if let Ok(meta) = fs::metadata(req_path) {
        let stat_uid = meta.uid();
        if stat_uid == 0 || stat_uid == 1000 {
            return Some(stat_uid);  // root and system - trusted
        }
        // AOSK_PKG is also trusted (our own app)
        if let Some(auid) = get_app_uid_fast(AOSK_PKG) {
            if stat_uid == auid {
                return Some(stat_uid);
            }
        }
    }
    None  // Failed to determine UID
}

Request Parsing

pid: lines are filtered from the request before command processing:

let req = raw
    .lines()
    .filter(|l| !l.starts_with("pid:"))
    .collect::<Vec<_>>()
    .join("\n")
    .trim()
    .to_string();

If pid: lines are not filtered, the daemon would attempt to execute pid:12345 as a command and return an error.


Mount Namespace Visibility

This is the heart of the AOSK security model. Every Android application runs in its own mount namespace. AOSK mounts su only in granted apps' namespaces.

How it works

namespace_watcher() - background thread running every 2 seconds:

1. Read current grant list
2. For each granted package:
   - Find all PIDs via /proc (UID match + cmdline match)
   - Get mount namespace inode via stat(/proc/PID/ns/mnt)
3. For each new namespace:
   - mount_su_for_pid(pid):
     a. nsenter -t PID -m -- sh -c "
          mkdir -p /system/xbin
          mount -t tmpfs tmpfs /system/xbin
          cp /data/local/tmp/su /system/xbin/su
          chmod 755 /system/xbin/su
          touch /system/bin/su 2>/dev/null
          mount --bind /data/local/tmp/su /system/bin/su 2>/dev/null
          # Also mount in /data/data/pkg/files/usr/bin/su if it exists
          if [ -d /data/data/PKG/files/usr/bin ]; then
            touch /data/data/PKG/files/usr/bin/su 2>/dev/null
            mount --bind /data/local/tmp/su /data/data/PKG/files/usr/bin/su 2>/dev/null
          fi
        "
4. For each namespace that is no longer granted:
   - unmount_su_for_pid(pid):
     nsenter -t PID -m -- sh -c "umount /system/xbin 2>/dev/null"
5. Clean up dead namespaces (process no longer exists)

Result

Application /system/xbin/su /system/bin/su which su
Granted (Terminal) ✅ exists, executable ✅ bind mount ✅ found
Not granted (Bank) ❌ does not exist ❌ does not exist ❌ not found
Not granted (Game) ❌ does not exist ❌ does not exist ❌ not found

Optimization: check before mounting

Before mounting, the daemon checks whether su has already been mounted in the process namespace:

let check_out = Command::new("nsenter")
    .args(["-t", &pid.to_string(), "-m", "--"])
    .arg("/system/bin/sh")
    .arg("-c")
    .arg("test -x /system/bin/su -o -x /system/xbin/su")
    .status();
if let Ok(st) = check_out {
    if st.success() {
        return true;  // su already visible, no need to mount
    }
}

Additional su in data directory

For apps that search for su in non-standard paths, the daemon also mounts su at /data/data/<pkg>/files/usr/bin/su:

if uid >= 10000 {
    if let Some(pkg) = get_pkg_for_uid_fast(uid) {
        let app_bin = format!("/data/data/{}/files/usr/bin", pkg);
        let app_su = format!("{}/su", app_bin);
        script.push_str(&format!(
            " && if [ -d {d} ]; then touch {t} 2>/dev/null; mount --bind {s} {t} 2>/dev/null; fi",
            d = app_bin, s = su_src, t = app_su
        ));
    }
}

Minfilter - Detector Evasion

Minfilter is a proactive evasion mechanism that hides suspicious folders and files from targeted applications.

How it works

  1. User specifies target applications (e.g. banking apps)
  2. Specifies folders to hide (e.g. /sdcard/TWRP, /data/adb)
  3. Specifies packages to hide (e.g. com.topjohnwu.magisk)
  4. Daemon performs mount --bind of an empty file onto each target folder in the target app's mount namespace

Implementation

fn minfilter_for_pkg(pkg: &str, folders: &str, grants: &Arc<Mutex<HashSet<String>>>) -> String {
    // Remove from grants (minfiltered apps should not have root)
    grants.lock().unwrap().remove(pkg);
    save_grants(&grants.lock().unwrap());
    
    // For each PID of the target app
    for pid in get_pids_for_package(pkg) {
        hide_folders_for_pid(pid, folders);
    }
}

fn hide_folders_for_pid(pid: i32, folders: &str) -> Result<(), String> {
    let empty = "/data/local/tmp/aosk_empty";
    let _ = fs::File::create(empty);  // Empty file
    
    enter_ns_and(pid, || {
        for f in folders.split(',') {
            let f = f.trim();
            if !f.is_empty() {
                // Bind-mount empty file onto target folder
                let _ = Command::new("mount")
                    .args(["-o", "bind", empty, f])
                    .status();
            }
        }
    })
}

Preset values (via Default button)

Target applications:

ru.sberbankmobile     (SberBank)
ru.tinkoff.cr         (Tinkoff)
com.alfabank.qapp     (Alfa-Bank)
ru.vtb.msa            (VTB)

Hidden folders:

/sdcard/TWRP          (TWRP recovery)
/sdcard/Fox           (Fox recovery)
/data/adb             (Magisk/KernelSU data)
/sbin                 (Magisk tmpfs)

Hidden packages:

com.topjohnwu.magisk  (Magisk)
org.lsposed.manager   (LSPosed)
me.weishu.kernelsu    (KernelSU)
io.github.apatcher    (APatcher)

enter_ns_and - entering a foreign namespace

fn enter_ns_and<F, R>(pid: i32, f: F) -> Result<R, String>
where F: FnOnce() -> R,
{
    // Save own namespace
    let self_ns = fs::File::open("/proc/self/ns/mnt")?;
    // Open target process namespace
    let target_ns = fs::File::open(&format!("/proc/{}/ns/mnt", pid))?;
    // Switch
    unsafe { libc::setns(target_ns.as_raw_fd(), CLONE_NEWNS)?; }
    // Perform operation
    let result = f();
    // Switch back
    unsafe { libc::setns(self_ns.as_raw_fd(), CLONE_NEWNS); }
    Ok(result)
}

SELinux and System Properties

SELinux

The daemon sets setenforce 0 on startup. This is Permissive mode, which:

  • Allows all operations but logs violations
  • Permits aoskd to execute mount, nsenter, setns
  • Does not block file-based IPC

Ideally, AOSK should work in Enforcing mode with proper SELinux policies, but this requires separate work to create policy files.

System property spoofing

The daemon sets AOSP-compatible system property values:

Property Value Purpose
ro.boot.verifiedbootstate green Indicates verified boot is enabled
ro.boot.flash.locked 1 Indicates bootloader is locked
ro.secure 1 Indicates secure boot is enabled
ro.debuggable 0 Indicates non-debug build
ro.build.type user Indicates user (not userdebug) build
ro.build.tags release-keys Indicates release keys

These values are typical for factory AOSP images and contain no signs of root/modification. Banking detectors check these properties.


Daemon Autostart

AOSK ensures daemon autostart through three mechanisms:

1. Ramdisk injection (primary)

The patcher injects into the ramdisk:

  • aoskd.rc - init service definition (class late_start)
  • aoskd_start.sh - startup script (sleep 10 → copy → chmod → run)
  • Patched init.rc - adds import /aoskd.rc

On Android boot:

  1. Init parses init.rc, finds import /aoskd.rc
  2. Imports aoskd.rc, registers aoskd service
  3. After late_start phase (system loaded) init launches aoskd_start.sh
  4. The script copies aoskd from /sdcard/AOSKP/ to /data/local/tmp/ and runs it

2. ensure_daemon() in su client

When the user or an app calls su, the client automatically checks:

fn ensure_daemon() {
    // Check: is aoskd running?
    for entry in fs::read_dir("/proc") {
        if cmdline == "aoskd" { return; }  // Already running
    }
    
    // Not running - start it
    // Try /data/local/tmp/aoskd
    // If not there - try /sdcard/AOSKP/aoskd
    // nohup /data/local/tmp/aoskd >/dev/null 2>&1 &
}

3. BootReceiver (Android application)

After device boot, the BroadcastReceiver executes su -c 'echo boot', which triggers ensure_daemon().


Boot/init_boot Patching

Target partition detection

Boot partition search priority:

1. /dev/block/by-name/init_boot_a   ← Android 12+ with A/B
2. /dev/block/by-name/init_boot      ← Android 12+ without A/B
3. /dev/block/by-name/boot_a         ← Older Android with A/B
4. /dev/block/by-name/boot           ← Older Android without A/B

init_boot (Android 12+) contains only the ramdisk (GKI), boot contains kernel + ramdisk. The patcher handles both formats.

Boot image structure

Android boot image header (versions 0-3):

Offset  Size    Field
0       8       Magic "ANDROID!"
8       4       kernel_size
12      4       kernel_addr (v0-2) / ramdisk_size (v3+)
16      4       ramdisk_size (v0-2)
...
36      4       page_size (v0-2) / unused (v3+)
40      4       header_version
...

The patcher reads:

  1. Header (page_size bytes) - contains sizes and metadata
  2. Kernel data (kernel_size + padding)
  3. Ramdisk data (ramdisk_size) - this is what gets patched
  4. Rest (dtb, dtbo, etc.)

After modifying the ramdisk, the patcher:

  1. Updates ramdisk_size in the header
  2. Writes header + kernel + new_ramdisk + padding + rest
  3. Truncates file to original size: out_file.set_len(input_size)

Header v0-2 vs v3+

Field v0-2 v3+
page_size From header (usually 2048 or 4096) Always 4096
ramdisk_size_offset 16 12
extra fields yes (tags, second, etc.) minimal

Ramdisk Format

LZ4 legacy

Magic: 02 21 4C 18

LZ4 legacy block archive format:

[4 bytes magic: 02214C18]
[4 bytes block_size_1]
[block_size_1 bytes compressed data]
[4 bytes block_size_2]
[block_size_2 bytes compressed data]
...
[4 bytes 0]  ← end marker

Bit 31 of block_size = uncompressed flag:

  • (block_size & 0x7FFFFFFF) = data size
  • (block_size >> 31) & 1 = 1 → data is uncompressed (raw copy)

Decompression:

fn lz4_block_decompress(input: &[u8]) -> Option<Vec<u8>> {
    // Verify magic
    // For each block:
    //   Read block_size
    //   If block_size == 0 → end
    //   If uncompressed → raw copy
    //   Else → lz4::block::decompress(data, Some(BLOCK_SIZE))
    // Collect all blocks into output
}

Compression:

fn lz4_block_compress(input: &[u8]) -> Vec<u8> {
    // Magic: 0x184C2102 (LZ4 block magic)
    // Split into 8MB blocks
    // For each block:
    //   lz4::block::compress(chunk, HC level 12)
    //   If compressed is smaller → write block_size + compressed
    //   Else → write (size | 0x80000000) + raw
    // End marker: 0
}

LZ4 HC level 12 - maximum compression level. Typical delta after injection: ~208 bytes (even with added files, high compression compensates).

gzip

Magic: 1F 8B

Standard gzip. Decompression via flate2::GzDecoder, compression via flate2::GzEncoder with Compression::best().

Raw CPIO

If the string 070701 or 070702 is found in the first 4096 bytes - the ramdisk is uncompressed. Data is read/written directly.


Request Protocol

Request format (client → daemon)

A request is a text file named req_<PID>_<timestamp> in a queue directory.

pid:<client_PID>
<command>

Command types:

Command Description Example
su_cmd:<shell command> Execute command as root su_cmd:id
grant:<package> Grant root to package grant:com.termux
revoke:<package> Revoke root from package revoke:com.termux
minfilter:<pkg>:<folders> Hide folders from app minfilter:ru.sberbankmobile:/sdcard/TWRP,/data/adb
status Query daemon status status

Special cases:

  • The status command is available without authorization
  • The AOSK app (identified via /proc/PID/cmdline) has grant/revoke/minfilter privileges without authorization
  • pid: lines are NOT passed to the shell, they are filtered out

Response format (daemon → client)

A response is a binary file named req_<PID>_<timestamp>.resp.

[stdout bytes][stderr bytes][4 bytes: exit_code LE i32]

Response parsing (Kotlin):

private fun parseResponse(bytes: ByteArray): String {
    if (bytes.size >= 4) {
        val de = bytes.size - 4
        val ec = bytes.sliceArray(de..bytes.size - 1)
        val exitCode = (ec[0].toInt() and 0xFF) or
            ((ec[1].toInt() and 0xFF) shl 8) or
            ((ec[2].toInt() and 0xFF) shl 16) or
            ((ec[3].toInt() and 0xFF) shl 24)
        val output = String(bytes, 0, de, Charsets.UTF_8)
        if (exitCode != 0 && output.isEmpty()) {
            return "Error:exit=$exitCode"
        }
        return output.trim()
    }
    if (bytes.isNotEmpty()) {
        return String(bytes, Charsets.UTF_8).trim()
    }
    return ""
}

Text responses (for grant/revoke/status/minfilter):

The daemon uses write_resp() to write a text response + exit code:

fn write_resp(resp_path: &str, data: &str, exit_code: i32) {
    let mut r = Vec::new();
    r.extend_from_slice(data.as_bytes());
    r.extend_from_slice(&exit_code.to_le_bytes());
    let _ = fs::write(resp_path, &r);
}

Text response formats:

Command Success response Error response
su_cmd:cmd [stdout+stderr][exit_code LE] [error text][126 LE]
grant:pkg granted:pkg:mounted:N Error:empty_pkg
revoke:pkg revoked:pkg:unmounted:N Error:empty_pkg
minfilter:pkg:folders filtered:pkg:N Error:bad_minfilter
status granted:pkg1,pkg2:N -

Authorization

The daemon verifies authorization by UID and request content.

Authorization hierarchy

1. UID 0 (root)          → Always authorized
2. UID 1000 (system)     → Always authorized
3. AOSK_PKG              → Always authorized (own app)
4. Granted packages       → Authorized if UID is in grant list
5. Everyone else          → Denied

AOSK app identification

fn is_aosk_app_pid(pid: i32) -> bool {
    let cp = format!("/proc/{}/cmdline", pid);
    if let Ok(cmdline) = fs::read_to_string(&cp) {
        return cmdline.trim_matches('\0').contains(AOSK_PKG);
    }
    false
}

If a request comes from a PID whose cmdline contains com.aoskpatch, the daemon processes it as a privileged request without grant checking.

Authorization check

fn is_authorized(uid: u32, raw_content: &str, grants: &Arc<Mutex<HashSet<String>>>) -> bool {
    if uid == 0 { return true; }       // root
    if uid == 1000 { return true; }     // system
    
    // Check: from AOSK app?
    for line in raw_content.lines() {
        if line.starts_with("pid:") {
            if let Ok(pid) = line[4..].parse::<i32>() {
                if is_aosk_app_pid(pid) { return true; }
            }
        }
    }
    
    // Check: UID in grants?
    let g = grants.lock().unwrap();
    for pkg in g.iter() {
        if let Some(app_uid) = get_app_uid_fast(pkg) {
            if uid == app_uid { return true; }
        }
    }
    
    false
}

Grant Storage

Grants persist across reboots.

Grant file

Path: /data/local/tmp/aosk_grants

Format: one package per line

com.termux
com.aidlux
org.kde.necessitas

Operations

Reading:

fn load_grants() -> HashSet<String> {
    let mut set = HashSet::new();
    if let Ok(content) = fs::read_to_string(GRANTS_FILE) {
        for line in content.lines() {
            let pkg = line.trim().to_string();
            if !pkg.is_empty() {
                set.insert(pkg);
            }
        }
    }
    set
}

Writing:

fn save_grants(set: &HashSet<String>) {
    let content: String = set.iter().cloned().collect::<Vec<_>>().join("\n");
    let _ = fs::write(GRANTS_FILE, content);
    let _ = Command::new("chmod").args(["666", GRANTS_FILE]).status();
}

chmod 666 - so the AOSK app (untrusted_app) can read the grants file directly as a fallback.

Fast UID lookup

For fast UID determination of a package, the daemon uses /data/system/packages.list instead of the slower pm list packages -U:

fn get_app_uid_fast(pkg: &str) -> Option<u32> {
    // /data/system/packages.list format:
    // com.example 10123 0 /data/data/com.example ...
    if let Ok(content) = fs::read_to_string("/data/system/packages.list") {
        for line in content.lines() {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() >= 2 && parts[0] == pkg {
                if let Ok(uid) = parts[1].parse::<u32>() {
                    return Some(uid);
                }
            }
        }
    }
    // Fallback: pm list packages -U
    get_app_uid(pkg)
}

packages.list is read in O(n) but much faster than launching pm via Command::new().

Reverse lookup (UID → package)

fn get_pkg_for_uid_fast(target_uid: u32) -> Option<String> {
    if let Ok(content) = fs::read_to_string("/data/system/packages.list") {
        for line in content.lines() {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() >= 2 {
                if let Ok(uid) = parts[1].parse::<u32>() {
                    // UID may differ by 100000 (multi-user)
                    if (uid % 100000) == (target_uid % 100000) {
                        return Some(parts[0].to_string());
                    }
                }
            }
        }
    }
    None
}

Comparison modulo 100000 - for compatibility with multi-user Android, where a secondary user's UID = primaryUID + 100000 * userId.


Process Discovery

Finding PIDs for a package

The daemon uses two methods to find processes belonging to a package:

  1. UID match - if the package UID is known, find processes with matching UID in /proc/PID/status
  2. cmdline match - find /proc/PID/cmdline containing the package name
fn get_pids_for_package(pkg: &str) -> Vec<i32> {
    let mut pids = Vec::new();
    let target_uid = get_app_uid_fast(pkg);

    for entry in fs::read_dir("/proc") {
        let pid = parse entry name as i32;
        if pid <= 1 { continue; }

        let mut matched = false;

        // Method 1: UID match
        if let Some(t_uid) = target_uid {
            if let Some(uid) = get_uid_of_pid(pid) {
                if uid == t_uid || (uid >= 99000 && (uid % 100000) == (t_uid % 100000)) {
                    matched = true;
                }
            }
        }

        // Method 2: cmdline match (fallback)
        if !matched {
            if let Ok(cmdline) = fs::read_to_string(format!("/proc/{}/cmdline", pid)) {
                if cmdline.trim_matches('\0').contains(pkg) {
                    matched = true;
                }
            }
        }

        if matched { pids.push(pid); }
    }
    pids
}

UID from /proc

fn get_uid_of_pid(pid: i32) -> Option<u32> {
    let status = format!("/proc/{}/status", pid);
    let content = fs::read_to_string(&status).ok()?;
    for line in content.lines() {
        if line.starts_with("Uid:") {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() > 1 {
                return parts[1].parse().ok();  // Real UID
            }
        }
    }
    None
}

/proc/PID/status contains the Uid: field with four values: Real, Effective, Saved, FS UID. We take the first one (Real UID).


Resolved Issues

During development, a number of critical issues were resolved:

Kotlin daemon + Cyrillic paths

Problem: Building Kotlin with paths containing Cyrillic characters (e.g. /home/gen8elite/Документы/) breaks Gradle/Kotlin compiler.

Solution: Build from an ASCII path via ~/aosk_build_tmp/ with source copy and cleanup after.

/sdcard/ mounted with noexec

Problem: Binaries cannot be executed directly from /sdcard/ - the filesystem is mounted with noexec.

Solution: Binaries are stored in /sdcard/AOSKP/, but executed from /data/local/tmp/ after copying + chmod 755.

Abstract unix socket blocked by SELinux

Problem: abstract:\0aosk_root_socket - SELinux blocks connections from untrusted_app to su process.

Solution: Switch to file-based IPC. Files in /sdcard/AOSKP/aosk_queue/ are accessible to untrusted_app via sdcardfs.

sdcardfs UID mapping

Problem: stat() on /sdcard/ returns a fake UID. A file created by root (UID 0) shows UID 10539.

Solution: pid: line in request + /proc/PID/status for real UID. stat() UID is used only as fallback for root/system.

adb shell hangs with background process

Problem: adb shell "/data/local/tmp/aoskd &" - adb keeps the session open, the command does not return.

Solution: nohup /data/local/tmp/aoskd >/dev/null 2>&1 & + timeout on ADB command.

adb pull/push are not shell commands

Problem: adb pull/push run as direct adb subcommands, not through adb shell.

Solution: In the desktop GUI, tokio::process::Command::new("adb").args(["pull", ...]) is used directly.

GKI ramdisk does not contain init.rc

Problem: On GKI devices (Samsung GSI Project Infinity) first-stage init doesn't parse .rc from ramdisk root. init.rc in CPIO root is ignored.

Solution: The patcher creates aoskd.rc (which is picked up via import) and a minimal init.rc with import /aoskd.rc + on early-init exec. Working autostart: su ensure_daemon().

pid:123 in request broke parsing

Problem: The daemon tried to execute pid:12345 as a shell command and got an error.

Solution: Filter pid: lines before command processing.

If resolve_uid = None, client hangs on timeout

Problem: If the daemon cannot determine UID, it doesn't write a .resp file, and the client waits for a 10 second timeout.

Solution: Always write .resp with error Error:uid_resolve_failed if resolve_uid returns None.

LZ4 legacy ramdisk on Samsung GSI

Problem: Ramdisk on Samsung GSI is compressed with LZ4 legacy (magic 0x02214C18), not gzip. Using gzip decompression causes bootloop.

Solution: Auto-detection of format by magic bytes. LZ4 legacy → lz4::block::decompress, gzip → flate2::GzDecoder.

Bind mount file in /system/

Problem: mount --bind /source/su /system/xbin/su doesn't work if the target file doesn't exist. /system/ = read-only (dm-verity).

Solution: tmpfs overlay: mount -t tmpfs tmpfs /system/xbincp su /system/xbin/su. Also touch /system/bin/su 2>/dev/null && mount --bind for /system/bin/su.

Patcher increases image size

Problem: File injection + 16 byte marker + padding increase the output file, which may cause flashing issues.

Solution: out_file.set_len(input_size) - truncate to original size. LZ4 HC level 12 ensures minimal overhead.

lz4-sys requires CC for cross-compilation

Problem: lz4-sys crate (native C binding) requires CC_aarch64_linux_android environment variable for cross-compilation.

Solution: Switch to pure Rust lz4 crate (no C dependencies).

Package Visibility (Android 11+)

Problem: Without <queries> in the manifest, the app only sees itself and system packages.

Solution: Added <queries> block in AndroidManifest.xml with intent-filter for MAIN and VIEW.


Building from Source

Prerequisites

For Rust components (aoskd, patcher, aosk-gui):

  • Rust toolchain: rustup default stable
  • NDK r27c: ~/Android/Sdk/ndk-bundle/android-ndk-r27c/
  • aarch64-linux-android target: rustup target add aarch64-linux-android

For Android application:

  • Android SDK
  • Kotlin 2.0.0
  • Gradle 8.x
  • Android Gradle Plugin 8.5.0

Building aoskd and su

aoskd and su are the same binary. The operating mode is determined by argv[0].

Cross-compilation config (.cargo/config.toml):

[target.aarch64-linux-android]
linker = "/path/to/ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android35-clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]

Build:

cd aoskd
cargo build --release --target aarch64-linux-android

Result:

  • aoskd/target/aarch64-linux-android/release/aoskd - ARM64 binary

Install on device:

adb push aoskd/target/aarch64-linux-android/release/aoskd /sdcard/AOSKP/aoskd
adb shell "cp /sdcard/AOSKP/aoskd /data/local/tmp/aoskd && chmod 755 /data/local/tmp/aoskd"

Launch:

adb shell "nohup /data/local/tmp/aoskd >/dev/null 2>&1 &"

Building the patcher

The patcher can be built for two platforms:

  • aarch64-linux-android - for running on device
  • x86_64-unknown-linux-gnu - for running on PC

Build for device:

cd patcher
cargo build --release --target aarch64-linux-android

Build for PC:

cd patcher
cargo build --release

Dependencies:

  • lz4 = "1.28" - pure Rust LZ4 block compression/decompression
  • flate2 = "1.1" - gzip compression/decompression

Building the desktop GUI

Dependencies:

  • iced = "0.13" (with feature tokio) - GUI framework
  • tokio = "1" (with features process, io-util) - async runtime
  • rfd = "0.15" - file dialog (not yet used in UI)

Build:

cd aosk-gui
cargo build --release

Run:

./target/release/aosk-gui

Building the Android application

build.gradle.kts:

android {
    namespace = "com.aoskpatch"
    compileSdk = 34
    defaultConfig {
        applicationId = "com.aoskpatch"
        minSdk = 26
        targetSdk = 34
    }
}
dependencies {
    implementation("dev.mobile:dadb:1.2.10")  // ADB over Wi-Fi
    implementation("com.google.android.material:material:1.11.0")
}

Binaries in assets:

Before building the APK, copy the compiled binaries:

cp aoskd/target/aarch64-linux-android/release/aoskd app/src/main/assets/aoskd
cp patcher/target/aarch64-linux-android/release/patcher app/src/main/assets/aosk_patcher

The app extracts them on first launch to /sdcard/AOSKP/.

Build:

# If the path contains Cyrillic - use an ASCII symlink:
mkdir -p ~/aosk_build_tmp
cp -r . ~/aosk_build_tmp/
cd ~/aosk_build_tmp
./gradlew assembleDebug
cp app/build/outputs/apk/debug/app-debug.apk ~/aosk_build_tmp/

Important: Cyrillic paths break Kotlin compilation. Always build from an ASCII path.


Installation

Via desktop GUI

  1. Connect the device via USB or Wi-Fi ADB
  2. Run aosk-gui
  3. Enter the device IP (or leave empty for USB)
  4. Click "Check" to verify daemon status
  5. Click "Patch Boot Image"
  6. Wait for completion (log shows progress)
  7. Reboot the device

Via Android application

  1. Install the APK on the device
  2. Grant file access permission (MANAGE_EXTERNAL_STORAGE)
  3. Go to the "Install" tab
  4. Click "Check" to verify daemon
  5. Click "Patch" to patch the boot partition
  6. Reboot the device

Manually via ADB

Step 1: Preparation

adb root
adb disable-verity
adb reboot
# After reboot:
adb root
adb remount

Step 2: Copy binaries

adb push aoskd /sdcard/AOSKP/aoskd
adb push aosk_patcher /sdcard/AOSKP/aosk_patcher
adb shell "cp /sdcard/AOSKP/aoskd /data/local/tmp/aoskd && chmod 755 /data/local/tmp/aoskd"
adb shell "cp /sdcard/AOSKP/aosk_patcher /data/local/tmp/aosk_patcher && chmod 755 /data/local/tmp/aosk_patcher"

Step 3: Find boot partition

adb shell "ls -l /dev/block/by-name/init_boot_a 2>/dev/null || \
           ls -l /dev/block/by-name/init_boot 2>/dev/null || \
           ls -l /dev/block/by-name/boot_a 2>/dev/null || \
           ls -l /dev/block/by-name/boot 2>/dev/null"

Step 4: Backup

adb shell "dd if=/dev/block/by-name/init_boot of=/sdcard/AOSKP/aosk_boot_backup.img"
adb pull /sdcard/AOSKP/aosk_boot_backup.img /tmp/boot_backup.img

Step 5: Patch (on PC)

./aosk_patcher /tmp/boot_backup.img /tmp/boot_patched.img

Step 6: Flash

adb push /tmp/boot_patched.img /sdcard/AOSKP/boot_patched.img
adb shell "dd if=/sdcard/AOSKP/boot_patched.img of=/dev/block/by-name/init_boot"

Step 7: Start daemon

adb shell "nohup /data/local/tmp/aoskd >/dev/null 2>&1 &"

Step 8: Reboot

adb reboot

Usage

su from the command line

Single command:

su -c id
# uid=0(root) gid=0(root)

su -c "cat /proc/version"
# Linux version 5.15. ...

su -c "pm list packages -3"
# package:com.termux
# package:com.aidlux

Interactive mode:

su
id
# uid=0(root) gid=0(root)
whoami
# root
ls /data/adb/
# ...
exit

Without arguments (no -c):

su
# su: empty command - use su -c 'command'
# (exit code 1)

Managing grants

Via Android application:

  1. Open the "Apps" tab
  2. Find the app in the list (or use search)
  3. Toggle the switch for grant/revoke
  4. Changes are applied instantly

Via command line (file-based IPC):

# Grant
echo "pid:$$
grant:com.termux" > /sdcard/AOSKP/aosk_queue/req_$$_$(date +%s)

# Revoke
echo "pid:$$
revoke:com.termux" > /sdcard/AOSKP/aosk_queue/req_$$_$(date +%s)

# Check status
echo "status" > /sdcard/AOSKP/aosk_queue/req_$$_$(date +%s)
# Response: granted:com.termux:1

Via su (as root):

su -c "echo 'grant:com.termux' > /data/local/tmp/aosk_queue/req_$$_$(date +%s)"

Minfilter

Via Android application:

  1. Open the "Minfilter" tab
  2. Press "Default" for preset values (Sber, Tinkoff, Alfa, VTB)
  3. Configure target apps, folders, and packages
  4. Toggle "Minfilter enabled" switch

Via command line:

# Hide /sdcard/TWRP and /data/adb from SberBank
echo "pid:$$
minfilter:ru.sberbankmobile:/sdcard/TWRP,/data/adb" > /sdcard/AOSKP/aosk_queue/req_$$_$(date +%s)

Daemon status

# Via su
su -c "echo 'status' > /data/local/tmp/aosk_queue/req_$$_$(date +%s)"

# Via ADB
adb shell "pidof aoskd"

Project Structure

aoskpatch/
├── .gitignore
├── build.gradle.kts              # Root Gradle config (AGP 8.5.0, Kotlin 2.0.0)
├── settings.gradle.kts           # Gradle settings
├── gradle.properties
├── gradle/
│   └── wrapper/
├── gradlew
├── gradlew.bat
│
├── aoskd/                        # Daemon + su client
│   ├── Cargo.toml                # edition 2024, dep: libc 0.2.185
│   ├── .cargo/
│   │   └── config.toml           # Cross-compile: aarch64-linux-android
│   └── src/
│       └── main.rs               # ~806 lines
│                                   # daemon_main(), su_main()
│                                   # watcher(), namespace_watcher()
│                                   # resolve_uid(), is_authorized()
│                                   # mount_su_for_pid(), minfilter
│                                   # spoof_aosp_props(), ensure_daemon()
│
├── patcher/                      # Boot/init_boot image patcher
│   ├── Cargo.toml                # edition 2024, deps: lz4 1.28, flate2 1.1
│   ├── .cargo/
│   │   └── config.toml           # Cross-compile config
│   └── src/
│       └── main.rs               # ~640 lines
│                                   # CPIO parser/writer (pure Rust)
│                                   # LZ4 legacy + gzip decompress/compress
│                                   # inject_into_ramdisk()
│                                   # Boot image header parsing (v0-3)
│
├── aosk-gui/                     # Desktop GUI (Rust + iced 0.13)
│   ├── Cargo.toml                # deps: iced 0.13 (tokio), tokio, rfd
│   └── src/
│       └── main.rs               # ~637 lines
│                                   # AoskApp state, update(), view()
│                                   # do_patch(), do_check_daemon()
│                                   # adb_exec(), adb_exec_timeout()
│                                   # Dark theme, card UI
│
└── app/                          # Android application
    ├── build.gradle.kts          # compileSdk 34, minSdk 26, dadb 1.2.10
    └── src/
        └── main/
            ├── AndroidManifest.xml    # MANAGE_EXTERNAL_STORAGE, BOOT_COMPLETED, queries
            ├── assets/
            │   ├── aoskd             # ARM64 binary (daemon + su client)
            │   └── aosk_patcher      # ARM64 binary (boot patcher)
            ├── res/
            │   ├── layout/
            │   │   ├── activity_main.xml     # BottomNavigationView + FrameLayout
            │   │   ├── fragment_minfilter.xml # Minfilter tab
            │   │   ├── fragment_install.xml   # Install tab
            │   │   ├── fragment_apps.xml      # Apps tab (RecyclerView)
            │   │   └── item_app.xml           # App item
            │   ├── menu/
            │   │   └── bottom_nav_menu.xml    # Bottom nav menu
            │   ├── color/
            │   │   └── bottom_nav_color.xml   # Nav colors
            │   └── drawable/
            │       ├── ic_filter.xml          # Minfilter icon
            │       ├── ic_install.xml         # Install icon
            │       └── ic_apps.xml            # Apps icon
            └── java/
                └── com/
                    └── aoskpatch/
                        ├── MainActivity.kt    # Main activity (~461 lines)
                        ├── RootBridge.kt      # File IPC bridge (~207 lines)
                        ├── AdbBridge.kt       # ADB over Wi-Fi (~60 lines)
                        └── BootReceiver.kt    # Autostart (~20 lines)

Security

Threat model

Threat Defense
App tries to find su su does not exist in its mount namespace
App scans sockets/ports No sockets, no ports
App checks system properties AOSP value spoofing
App scans /sdcard/ IPC files are created and deleted in milliseconds
App checks mount points tmpfs mount in /system/xbin/ only visible in granted namespace
Root app tries to execute su Must be in grant list or be the AOSK app
Attacker replaces request file UID verified via /proc (not stat() on sdcard)
Race condition in IPC Unique file names (pid + timestamp)

Known risks

  1. SELinux Permissive - setenforce 0 disables SELinux enforcement. This is needed for mount/nsenter operations. In Enforcing mode, custom SELinux policy will be required.

  2. Files in /sdcard/ - although they are deleted quickly, an app could theoretically detect them during their brief existence. Mitigation: minimal file lifetime.

  3. /data/local/tmp/aosk_grants - file readable by all (chmod 666). A detector could check for its existence. Mitigation: can be hidden via minfilter.

  4. aoskd process - pidof aoskd or /proc/PID/cmdline could reveal the daemon. Mitigation: process name can be changed.


Compatibility

Supported Android versions

Version Boot partition Status
Android 12+ (GKI) init_boot ✅ Tested (Samsung GSI)
Android 11 boot ✅ Should work
Android 10 boot ✅ Should work
Android 9 boot ✅ Should work
Android 8 boot ✅ Should work

Supported firmware

Firmware Status Notes
AOSP Full support
GSI (Project Infinity) Tested on Samsung
Samsung One UI ⚠️ May require Knox disable
Xiaomi MIUI ⚠️ Not tested
OnePlus OxygenOS ⚠️ Not tested
Stock AOSP-based Full support

Device requirements

  • ARM64 (aarch64) processor
  • Unlocked bootloader
  • ADB access (for initial installation)
  • Root via ADB (adb root) for patching

Comparison with Magisk / KernelSU

Feature AOSK Magisk KernelSU
Security model Default-deny Default-allow Default-allow
Root hiding su invisible by default Zygisk DenyList Kernel level
IPC mechanism Files Socket + binder Kernel level
Requirements Boot patch Boot patch Custom kernel / GKI module
SELinux Permissive (setenforce 0) Magisk patch Kernel-level
Detectability Low (no sockets) Medium (known patterns) Low
Interface Android app + Desktop GUI Android app Android app
Compatibility Universal Universal GKI / custom kernel
Mount namespace Per-app su visibility Zygisk namespace Kernel namespace
Open source
Language Rust + Kotlin C++ + Kotlin C + Kotlin

Key differences

AOSK vs Magisk:

  • Magisk hides root from selected apps. AOSK shows root only to selected apps.
  • Magisk uses Zygisk for injection. AOSK uses mount namespaces.
  • Magisk is well-known to detectors. AOSK uses atypical mechanisms (file-based IPC).
  • Magisk leaves traces (files, properties). AOSK minimizes its footprint.

AOSK vs KernelSU:

  • KernelSU operates at the kernel level - more powerful, but requires a custom kernel or GKI module.
  • AOSK operates in userspace - easier to install, but depends on SELinux permissive.
  • KernelSU has finer control. AOSK has inverted model by default.

Known Limitations

  1. SELinux Permissive - AOSK requires setenforce 0. In Enforcing mode, many operations (mount, nsenter, setns) will be blocked. Custom SELinux policy is required for full functionality.

  2. adb root - initial installation and patching require ADB root. This may not be available on production devices.

  3. dm-verity - disable-verity is required. This may not work on devices with strict verified boot.

  4. LZ4 HC compression overhead - while LZ4 HC level 12 usually compresses well, if the ramdisk is already heavily compressed, adding files may not fit within the original size. The patcher truncates the file, which may cause data loss at the end.

  5. No Zygisk equivalent - AOSK does not inject code into app processes. It only manages su visibility and mount namespaces.

  6. Monitoring every 2 sec - namespace_watcher checks /proc every 2 seconds. Between checks, a newly granted app may not see su.

  7. Filesystem-based queue - file-based IPC is slower than sockets. Typical request-response: 200-500ms (including 100ms daemon poll interval).

  8. ARM64 only - build only for aarch64-linux-android. ARM32, x86, x86_64 are not supported.

  9. /sdcard/ noexec - binaries cannot run directly from /sdcard/, require copying to /data/local/tmp/.

  10. GKI ramdisk - on GKI devices, first-stage init doesn't parse .rc from ramdisk root. Ramdisk autostart may not work, fallback: ensure_daemon() in su client.


Roadmap

  • SELinux Enforcing mode support (custom policy)
  • Module system (analog of Magisk modules)
  • Zygisk-compatible API for modules
  • WebUI for browser-based management
  • ARM32, x86, x86_64 support
  • System-less approach (no /system/ modification)
  • Daemon auto-update
  • Grant backup/restore
  • Built-in SafetyNet/Play Integrity check
  • TWRP recovery integration
  • Uninstall protection (device admin)
  • Dynamic su binary name (random process name)
  • IPC file encryption
  • Android 15+ support (new restrictions)
  • CI/CD for automated builds

License

The AOSK project is distributed under an open license. All components are written from scratch without borrowing code from Magisk or KernelSU.


AOSK - root that doesn't exist, until you decide otherwise.

About

The Next-Gen Root Solution That Undetected to All Banks(and will be for next 3 years i swear)

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors