Status: reviewed
The Kernel Check detector asks whether the running kernel looks like a custom or community kernel rather than the vendor's build, and whether kernel-facing views agree. These are heuristics about the build; they do not prove root.
- Observable signal: the kernel release and version from the uname syscall,
uname, /proc/version, /proc/sys/kernel/osrelease and System.getProperty. - Producing subsystem: the kernel's UTS namespace and procfs.
- Mechanism: a module that hooks one identity path leaves the others disagreeing.
- References: kernel/common Documentation/filesystems/proc.rst (/proc/version and /proc/sys/kernel); system/core rootdir/init.rc (
hostname localhost, so uname output starts "Linux localhost"). - Applicability: every release.
- Visibility limits: none for these world-readable views.
- Result states: consistent, mismatch.
- Interpretation: a mismatch between identity sources is evidence of rewriting.
- Observable signal: emoji, CJK and other scripts, Telegram handles, @ mentions, known community identifiers and non-release major versions in the kernel identity.
- Producing subsystem: the kernel build's version string.
- Mechanism: community kernels usually brand their version strings.
- References: Discovery only: the marker catalog comes from observed community kernel strings.
- Applicability: every release.
- Visibility limits: a custom kernel can use a vendor-looking string.
- Result states: detected, clean.
- Interpretation: markers are heuristics and stay below direct evidence.
- Observable signal: critical options in /proc/cmdline and whether kernel pointers are exposed.
- Producing subsystem: the bootloader's command line and the kernel's kptr_restrict setting.
- Mechanism: permissive or debug options and exposed pointers are unusual on user builds.
- References: kernel/common Documentation/filesystems/proc.rst (/proc/cmdline and /proc/kallsyms).
- Applicability: /proc/cmdline is readable only where SELinux allows it.
- Visibility limits: unreadable sources are reported as partial.
- Result states: detected, clean, unavailable.
- Interpretation: a permissive command line is danger; pointer exposure is a warning.
- Observable signal: whether /sdcard/Android/data can be listed through a path with ignorable Unicode codepoints.
- Producing subsystem: MediaProvider's FUSE path handling for shared storage.
- Mechanism: an unpatched path filter treats a path with an ignorable codepoint as a different, unprotected directory.
- References: Discovery only: the report labels this CVE-2024-43093 after the Android security bulletin; the MediaProvider fix itself was not reviewed.
- Applicability: devices before the fix.
- Visibility limits: storage permissions and scoped storage change what the probe can list.
- Result states: unpatched, partially patched, patched, inconclusive.
- Interpretation: a working bypass is a patch-level finding, not a kernel finding.
- Observable signal: MIDR_EL1 read through the kernel's EL0 MRS emulation on each logical CPU, against the identity the kernel cached for that CPU in its sysfs midr_el1 node or its /proc/cpuinfo block.
- Producing subsystem: the kernel's per-CPU cpuinfo_arm64 record and its MRS emulation.
- Mechanism: a read pinned to a CPU, and confirmed by getcpu() to have run there, exposes a cached identity that was rewritten. Pinning alone is not enough: moving the app to another cpuset or pausing a CPU moves the thread, and a read taken on another core of a heterogeneous SoC would disagree for that reason alone, so such reads are discarded and a value counts only when three confirmed reads agree.
- References: kernel/common Documentation/arch/arm64/cpu-feature-registers.rst (MIDR_EL1 holds the value of the CPU it is fetched on, racy without affinity) and arch/arm64/kernel/cpufeature.c on android13-5.15 and android15-6.6 (the emulated value is read_cpuid_id()); kernel.org v6.12 arch/arm64/kernel/cpuinfo.c (sysfs midr_el1 and /proc/cpuinfo both print cpu_data[cpu].reg_midr, stored when the CPU comes online); kernel.org v5.10 and v6.12 arch/arm64/kernel/entry-common.c (the emulation runs with interrupts enabled); kernel.org v5.10 to v6.12 kernel/cgroup/cpuset.c (cpuset_attach() resets the affinity of moved tasks; 6.6 and 6.12 keep a requested mask only where it intersects the new cpuset, 6.1 does not) and kernel/sched/core.c (select_fallback_rq() moves pinned tasks off inactive CPUs); kernel.org v6.1 kernel/sys.c (getcpu() reports the CPU it runs on); kernel.org v6.12 and v6.15 arch/arm64/include/asm/el2_setup.h and arch/arm64/kvm/hyp/include/hyp/sysreg-sr.h (what a KVM guest reads as MIDR_EL1); system/core libprocessgroup task_profiles.cpp and task_profiles.json (app threads join the top-app, foreground and background cpusets); bionic libc/SYSCALLS.TXT (bionic calls getcpu, so app seccomp allows it); the Arm Architecture Reference Manual defines the register but was not consulted here.
- Applicability: arm64 only; other ABIs report it as unavailable.
- Visibility limits: kernels without the register exposure cannot be compared, and CPUs outside the app's cpuset are not visited. KVM gives a guest the MIDR_EL1 of the physical CPU its vCPU is running on unless the VMM sets the guest's implementation ID registers, which Linux 6.15 allows and 6.12 does not, so a guest on a heterogeneous host can disagree with its own cached identity without any rewriting.
- Result states: consistent, mismatch, partial, unavailable.
- Interpretation: a confirmed mismatch is danger. CPUs whose reads could not be confirmed on the pinned CPU, or changed between reads, are not compared and leave the result partial.
- The storage path filter bypass is a MediaProvider patch check that lives in the kernel detector; see docs/architecture/follow-ups.md.