JingMatrix / JingMatrix/TEESimulator

Injector walks all of /proc every 2 s for the whole uptime (~1.4% of a core)

Open
#286 0 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
2.4k
Forks
312
Avg merge
2d 8h
Merged PRs (30d)
1

Description

Hi! First of all, thanks for TEESimulator. It's the first attestation shim on my S25 Ultra that survived every bank I threw at it.

I was chasing battery drain on the phone and teesim kept showing up in batterystats under uid 0. Turned out the daemon burns CPU around the clock even when nothing touches the keystore, and it comes down to one loop.

What's happening

Injector.loop() calls findPid("keystore2") on every iteration, sleeps 2 s, repeats. findPid lists /proc and reads cmdline of every process. On a Samsung ROM that's ~1000 processes, so each pass is a few hundred reads, all to find a pid that hasn't changed since boot (keystore2 has been pid 1182 here for 8 days).

I measured the teesim-injector thread directly via /proc/<pid>/task/<tid>/{stat,io,status} deltas, daemon idle, screen off:

per 20 s
wakeups 9
read syscalls ~3240 (~360 per pass)
CPU 28 ticks, about 1.4% of a core, all the time

Over a week that added up to ~2 h of CPU for the daemon, and teesim-injector was by far its hottest thread. The old logcat child was the other half of the story, but #266 already fixed that. Thanks, canary-63 works great here.

Build: 52 / 638530c originally; the loop is the same in current dev (123d8ba).

Fix

Keep the loop and its semantics, just don't walk /proc while the pid we injected is still alive. One cmdline read on the known pid, and fall back to the full walk only when it's gone or got recycled:

diff --git a/app/src/main/java/org/matrix/teesim/Injector.kt b/app/src/main/java/org/matrix/teesim/Injector.kt
index 8290e13..ab96063 100644
--- a/app/src/main/java/org/matrix/teesim/Injector.kt
+++ b/app/src/main/java/org/matrix/teesim/Injector.kt
@@ -46,3 +46,8 @@ class Injector(private val moduleDir: File) {
         while (running) {
-            val pid = findPid(procName)
+            // The full /proc walk in findPid touches every process's cmdline (~1000 reads on a
+            // busy device) and used to run every 2s for the whole uptime, measured at ~1.4% of a
+            // core continuously, just to rediscover a pid that never changes between keystore
+            // restarts. Once injected, verify the known pid with one cmdline read and only fall
+            // back to the walk when the process is gone or reused.
+            val pid = if (lastPid > 0 && isNamedProcess(lastPid, procName)) lastPid else findPid(procName)
             // Tell the log tail which process to capture, so the Logs panel shows the target
@@ -150,2 +155,19 @@ class Injector(private val moduleDir: File) {
 
+    /**
+     * True if /proc/[pid]/cmdline still names [name]: the cheap liveness probe that lets the loop
+     * skip the full /proc walk while the injected keystore is alive. A vanished or recycled pid
+     * (different cmdline) reads as false, which sends the loop back to [findPid].
+     */
+    private fun isNamedProcess(pid: Int, name: String): Boolean {
+        val cmd =
+            try {
+                File("/proc/$pid/cmdline").readBytes()
+            } catch (e: Exception) {
+                return false
+            }
+        if (cmd.isEmpty()) return false
+        val end = cmd.indexOf(0.toByte()).let { if (it < 0) cmd.size else it }
+        return String(cmd, 0, end).substringAfterLast('/') == name
+    }
+
     private fun sleep(ms: Long) {

Re-injection on keystore restart still happens within one 2 s tick (pid vanishes, walk, new pid != lastPid, inject). Failure back-off and LogTail.targetPid are untouched.

Result on the same device, same measurement, patched dex on canary-63 (daemon restarted, hook re-injected, control: ack ok=true):

teesim-injector, per 60 s before after
CPU ticks ~75 1-2
read syscalls ~10 800 90
wakeups 30 30

Whole daemon went from ~91 to ~8 ticks/min. Happy to turn this into a PR if you prefer, or just take the diff.

Side note: building app/ on Windows

Two things bit me while building the dex on Windows, in case you want them (both are no-ops on Linux):

aidl import paths: aidl.exe appends the host separator to each -I and compares strings, so CMake's D:/x/aidl never matches the input's package dir and every import fails with "directory ... is not found in any of the import paths". Also the auto-detect probes build-tools/*/aidl without .exe.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ea0afa8..3c77ca9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -89,7 +89,27 @@ if(NOT EXISTS ${AIDL_GEN}/.done)
   file(MAKE_DIRECTORY ${AIDL_GEN}/src ${AIDL_GEN}/include)
+  # aidl resolves an input's package directory against the -I list by string comparison after
+  # appending the host separator, so on Windows a CMake-style "D:/x/aidl" import path becomes
+  # "D:/x/aidl\" and never matches the input's "D:/x/aidl/..." and every import fails with
+  # "directory ... is not found in any of the import paths". Hand aidl native paths on Windows.
+  set(_km_i ${KM_AIDL})
+  set(_sc_i ${SC_AIDL})
+  set(_ss_i ${SS_AIDL})
+  if(CMAKE_HOST_WIN32)
+    file(TO_NATIVE_PATH "${KM_AIDL}" _km_i)
+    file(TO_NATIVE_PATH "${SC_AIDL}" _sc_i)
+    file(TO_NATIVE_PATH "${SS_AIDL}" _ss_i)
+  endif()
   foreach(pkg keymint secureclock sharedsecret)
     file(GLOB _aidls ${SEC}/${pkg}/aidl/android/hardware/security/${pkg}/*.aidl)
+    if(CMAKE_HOST_WIN32)
+      set(_native_aidls)
+      foreach(_f ${_aidls})
+        file(TO_NATIVE_PATH "${_f}" _nf)
+        list(APPEND _native_aidls "${_nf}")
+      endforeach()
+      set(_aidls ${_native_aidls})
+    endif()
     execute_process(
       COMMAND ${AIDL_BIN} --lang=ndk --structured --stability=vintf --min_sdk_version=29
-              -I ${KM_AIDL} -I ${SC_AIDL} -I ${SS_AIDL}
+              -I ${_km_i} -I ${_sc_i} -I ${_ss_i}
               -o ${AIDL_GEN}/src -h ${AIDL_GEN}/include ${_aidls}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 239d2d9..4233f8b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -67,2 +67,4 @@ android {
                     )
+                // Windows: CMake's auto-detect probes build-tools/*/aidl without the .exe suffix.
+                arguments += listOf("-DAIDL_BIN=D:/Mobile/AndroidBuild/sdk/build-tools/36.0.0/aidl.exe")
             }
bindgen / libclang: the NDK doesn't ship libclang.dll, and cargo-ndk 4 overwrites BINDGEN_EXTRA_CLANG_ARGS_<target> wholesale and sets CLANG_PATH to the extension-less NDK clang, which clang-sys rejects. CPATH is the only thing that passes through. Partial: with this the daemon dex builds fine via :app:dex, but the Rust TA still trips on stdint.h ordering under CPATH, so I didn't push further.
diff --git a/rust/build.sh b/rust/build.sh
index 922e074..26464b8 100755
--- a/rust/build.sh
+++ b/rust/build.sh
@@ -55,4 +55,16 @@ fi
 if [ -z "${LIBCLANG_PATH:-}" ]; then
-  for p in /usr/lib /usr/lib64 /usr/lib/llvm/lib /usr/lib/x86_64-linux-gnu; do
-    [ -e "$p/libclang.so" ] && export LIBCLANG_PATH="$p" && break
+  # Linux distros ship libclang.so. On Windows bindgen needs libclang.dll, which the NDK does not
+  # bundle, so also probe an LLVM install (winget LLVM.LLVM lands in "C:\Program Files\LLVM").
+  for p in /usr/lib /usr/lib64 /usr/lib/llvm/lib /usr/lib/x86_64-linux-gnu \
+           "${LLVM_HOME:-}/bin" "/c/Program Files/LLVM/bin" "${ProgramFiles:-}/LLVM/bin"; do
+    { [ -e "$p/libclang.so" ] || [ -e "$p/libclang.dll" ]; } && export LIBCLANG_PATH="$p" && break
+  done
+fi
+# On Windows bindgen cannot learn clang's builtin include dir (stddef.h & co.) the usual way:
+# cargo-ndk 4 sets CLANG_PATH to the NDK's extension-less `clang`, which clang-sys rejects, and it
+# overwrites BINDGEN_EXTRA_CLANG_ARGS_<target> wholesale, so nothing can be appended there. CPATH
+# passes through untouched and libclang honours it, so point it at the LLVM install's resource dir.
+if [ -z "${CPATH:-}" ] && [ -e "${LIBCLANG_PATH:-}/libclang.dll" ]; then
+  for d in "$(dirname "$LIBCLANG_PATH")"/lib/clang/*/include; do
+    [ -e "$d/stddef.h" ] && export CPATH="$d" && break
   done

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with app/src/main/java/org/matrix/teesim/Injector.kt, especially Injector.loop(), findPid(), and the proposed isNamedProcess() path; compare its behavior with the reported CPU and read-syscall measurements. Validate reinjection after keystore2 restarts and the unchanged LogTail.targetPid and failure back-off behavior. The Windows build notes separately identify CMakeLists.txt, app/build.gradle.kts, and rust/build.sh; :app:dex is the stated build check, while the Rust TA remains unresolved.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, cmake, kotlin, rust, shell
Domain
build-system, mobile-dev, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.