Skip to content

Replace the loopback port and abstract socket with pathname sockets - #264

Merged
JingMatrix merged 2 commits into
devfrom
control-sockets
Aug 20, 2026
Merged

Replace the loopback port and abstract socket with pathname sockets#264
JingMatrix merged 2 commits into
devfrom
control-sockets

Conversation

@JingMatrix

Copy link
Copy Markdown
Owner

The daemon exposes two local control channels -- the WebUI's key-management endpoint (KeyAdmin) and the daemon<->interceptor config channel -- and both were bound on transports any uid on the device can reach. KeyAdmin listened on 127.0.0.1:8790; a loopback TCP port has no owner, so authentication can only sit above the transport and the socket itself stays open for anyone to connect and speak to. The config channel bound the abstract unix name \0teesim; the abstract namespace ignores filesystem permissions and is a flat global address space, so any process can address it, and even its existence leaks through connect() -- a bound name is refused with EACCES, an unbound one with ECONNREFUSED, which separates "present" from "absent" without any access at all.

A pathname socket instead inherits the access control of the directory that holds it. Under a directory whose search bit is denied to unprivileged uids the path cannot be traversed, so connect() returns EACCES whether or not the node exists -- nothing to enumerate, and nothing to tell apart from an empty directory.

KeyAdmin now binds /data/adb/teesim/admin.sock inside the module's 0700 directory. A WebView cannot open a unix socket, so the WebUI reaches it through the manager's root shell and a small HTTP-over-UDS client (teesim-uds, built per abi and staged at a fixed path); the request and response wire format is unchanged, only the socket underneath moved. The endpoint keeps its random admin token and now also refuses any peer whose uid is not root at accept.

The config channel (common/control.cpp) now binds /data/misc/keystore/.teesim-ctl. It has to live in keystore's own directory rather than the module's: the interceptor binds from inside keystore (uid 1017, which cannot traverse root's 0700 /data/adb), and /data/misc/keystore is a 0700 directory keystore owns. sepolicy grants keystore the sock_file create and bind and the root daemon the traversal and connect; the existing SO_PEERCRED root check on the server is untouched.

…ds cannot reach them

The daemon exposes two local control channels -- the WebUI's key-management endpoint (KeyAdmin) and the daemon<->interceptor config channel -- and both were bound on transports any uid on the device can reach. KeyAdmin listened on `127.0.0.1:8790`; a loopback TCP port has no owner, so authentication can only sit above the transport and the socket itself stays open for anyone to connect and speak to. The config channel bound the [abstract unix name](https://man7.org/linux/man-pages/man7/unix.7.html) `\0teesim`; the abstract namespace ignores filesystem permissions and is a flat global address space, so any process can address it, and even its existence leaks through `connect()` -- a bound name is refused with `EACCES`, an unbound one with `ECONNREFUSED`, which separates "present" from "absent" without any access at all.

A pathname socket instead inherits the access control of the directory that holds it. Under a directory whose search bit is denied to unprivileged uids the path cannot be traversed, so `connect()` returns `EACCES` whether or not the node exists -- nothing to enumerate, and nothing to tell apart from an empty directory.

KeyAdmin now binds `/data/adb/teesim/admin.sock` inside the module's 0700 directory. A WebView cannot open a unix socket, so the WebUI reaches it through the manager's root shell and a small HTTP-over-UDS client (`teesim-uds`, built per abi and staged at a fixed path); the request and response wire format is unchanged, only the socket underneath moved. The endpoint keeps its random admin token and now also refuses any peer whose uid is not root at `accept`.

The config channel (`common/control.cpp`) now binds `/data/misc/keystore/.teesim-ctl`. It has to live in keystore's own directory rather than the module's: the interceptor binds from inside keystore (uid 1017, which cannot traverse root's 0700 `/data/adb`), and `/data/misc/keystore` is a 0700 directory keystore owns. sepolicy grants keystore the `sock_file` create and bind and the root daemon the traversal and connect; the existing `SO_PEERCRED` root check on the server is untouched.
… behind

Moving the two control channels onto filesystem sockets updated the code but not
the prose around it. The daemon<->interceptor channel was still documented as the
abstract name `@teesim` in control.h, control.cpp, both entry points, App.kt and
three READMEs, and KeyAdmin was still described as a loopback listener in the
webroot transport seam and in KeyAdmin's own body-size comment -- neither address
exists any more, so a reader following the comments would look for a socket that
was never bound. Each now names the control socket, or the root-only socket, and
lets Const.kt and control.cpp be the single places a path is written down.

Control.kt also spelled keystore's uid as the literal 1017 next to the constant
that already holds it, so it now reads Const.AID_KEYSTORE. That constant's own doc
says where the name comes from: AOSP's AID_KEYSTORE in android_filesystem_config.h,
mirrored here because the framework's Process.KEYSTORE_UID is @hide and the public
SDK exposes only the root/system/phone/shell uids and the application-uid bounds.

Comments and documentation only; no behaviour change.
@JingMatrix JingMatrix changed the title Move the control channels onto root-only filesystem sockets so app uids cannot reach them Replace the loopback port and abstract socket with pathname sockets Aug 20, 2026
@JingMatrix
JingMatrix merged commit c69e9e3 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