Skip to content

Read logd in-process and recover the admin socket from a dead fd - #266

Merged
JingMatrix merged 1 commit into
devfrom
fix/logd-in-process-reader
Aug 20, 2026
Merged

Read logd in-process and recover the admin socket from a dead fd#266
JingMatrix merged 1 commit into
devfrom
fix/logd-in-process-reader

Conversation

@JingMatrix

Copy link
Copy Markdown
Owner

Two failures compounded once the KeyAdmin listen fd was closed under the daemon (#265) -- on the reporter's device, MIUI power management reclaiming it on screen-off. The accept loop caught the resulting EBADF and immediately retried on the same dead fd, with no backoff and a full stack trace each time: ~7000 throws a second, ~2.4 cores, and a logcat flood that pushed logd to drop other readers' entries. The socket node stayed on disk with no listener behind it, so the WebUI got ECONNREFUSED and never recovered -- the daemon stays alive under Looper.loop, so service.sh's respawn never fires.

serve() now binds through a bindListener() helper and, on accept failure, backs off (capped at 1s), logs only the first failure of a burst, and rebinds a fresh LocalServerSocket after three consecutive failures. A one-time external fd close now costs a short pause and a rebind instead of a permanent spin, and the WebUI comes back on its own.

The logging path made the flood worse: LogTail spawned a logcat -v threadtime *:I child, so logd copied every app's info-and-above line into a pipe that the JVM then regex-filtered -- the whole device stream carried at full volume and amplified by the reader meant to observe it. It is replaced by an in-process native reader (libteesim_logcat.so) built on liblog's reader API, the same one logcat uses. It reads logd directly, filters in C to the lines we keep (TEESimulator, AndroidRuntime, fatal-level, the crash buffer, and the injected keystore pid), and serves them two ways: a bounded ring the WebUI polls incrementally over the unchanged /logs cursor, and rotating files under /data/adb/teesim/log so a crash's context survives a restart. /logs/download now streams those files rather than the in-memory ring. The reader emits nothing through logd, so it can never observe and re-capture itself.

The liblog reader symbols are absent from the NDK stub; they are declared locally and left undefined at link (-Wl,-z,undefs), resolving at runtime against the device's real liblog. sepolicy grants the daemon's root-manager domain the logd connect and logdr_socket write it now needs directly.

Two failures compounded once the KeyAdmin listen fd was closed under the daemon
(#265) -- on the reporter's device, MIUI power management reclaiming it on
screen-off. The accept loop caught the resulting EBADF and immediately retried on
the same dead fd, with no backoff and a full stack trace each time: ~7000 throws
a second, ~2.4 cores, and a logcat flood that pushed logd to drop other readers'
entries. The socket node stayed on disk with no listener behind it, so the WebUI
got ECONNREFUSED and never recovered -- the daemon stays alive under Looper.loop,
so service.sh's respawn never fires.

serve() now binds through a bindListener() helper and, on accept failure, backs
off (capped at 1s), logs only the first failure of a burst, and rebinds a fresh
LocalServerSocket after three consecutive failures. A one-time external fd close
now costs a short pause and a rebind instead of a permanent spin, and the WebUI
comes back on its own.

The logging path made the flood worse: LogTail spawned a `logcat -v threadtime
*:I` child, so logd copied every app's info-and-above line into a pipe that the
JVM then regex-filtered -- the whole device stream carried at full volume and
amplified by the reader meant to observe it. It is replaced by an in-process
native reader (libteesim_logcat.so) built on liblog's reader API, the same one
`logcat` uses. It reads logd directly, filters in C to the lines we keep
(TEESimulator, AndroidRuntime, fatal-level, the crash buffer, and the injected
keystore pid), and serves them two ways: a bounded ring the WebUI polls
incrementally over the /logs cursor, and rotating files under
/data/adb/teesim/log so a crash's context survives a restart. /logs/download now
streams those files rather than the in-memory ring. The reader emits nothing
through logd, so it can never observe and re-capture itself.

The liblog reader symbols are absent from the NDK stub; they are declared locally
and left undefined at link (-Wl,-z,undefs), resolving at runtime against the
device's real liblog. sepolicy grants the daemon's root-manager domain the logd
connect and logdr_socket write it now needs directly.

This is the first in-process JNI in the daemon, so the release build needs a
keep rule: R8 lists only the custom proguard file (no getDefaultProguardFile), so
the stock "keep native method names" rule was missing and R8 renamed LogTail and
its natives -- ART then looked up Java_<obfuscated> and the daemon crash-looped
with UnsatisfiedLinkError on the log-reader thread while the .so still exported
the plain Java_org_matrix_teesim_LogTail_* names. The rule is restored, and the
reader thread now contains any throwable so a log-reader failure disables the
Logs panel instead of taking the whole daemon down.
@JingMatrix
JingMatrix force-pushed the fix/logd-in-process-reader branch from 474ec43 to 675aad4 Compare August 20, 2026 10:30
@JingMatrix
JingMatrix merged commit fa2f863 into dev Aug 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant