fix: F-010: authenticate token-socket peer before sending auth token (#140)

This commit is contained in:
Iuri Matias
2026-06-08 13:52:44 -04:00
committed by GitHub
parent 51313eb58f
commit f1debca480
4 changed files with 293 additions and 3 deletions
+1
View File
@@ -233,6 +233,7 @@ Takes callback functions (`IsKnownFn`, `GetDependenciesFn`) so it has no couplin
- Async read loop for stdout/stderr with line buffering
- Synchronous kill with graceful SIGTERM → SIGKILL escalation (5s timeout)
- Unix domain socket for token delivery (scoped by `LOGOS_INSTANCE_ID`)
- **Token-listener authentication (CWE-940):** the socket path is predictable and world-writable, so before writing the auth token `sendTokenToProcess()` verifies the connected peer's credentials. The peer uid must match ours and, when the child pid is known, the peer pid must equal the spawned child — read via `SO_PEERCRED` on Linux and via `getpeereid()` + `getsockopt(SOL_LOCAL, LOCAL_PEERPID)` on macOS, so both platforms enforce the uid + pid gate. A mismatched peer is treated like a failed connect: the token is never written and the send fails closed, so a co-tenant pre-binding the path cannot intercept the secret. The named-path race is closed completely only by a future `socketpair()`-fd handoff.
- A `std::mutex` (`s_processesMutex`) protects the `s_processes` map against concurrent access
**ModuleContainer interface:** `id()``"subprocess"`, `canHandle()`, `launch()`, `sendToken()`, `terminate()`, `terminateAll()`, `hasModule()`, `pid()`, `getAllPids()`
+3 -1
View File
@@ -97,6 +97,8 @@ Each module runs in its own process for isolation:
Since the remote object registry has no built-in security mechanisms, all RPC calls require an authentication token. This is transparent to module developers when using the SDK:
1. **Core → Module**: When a module is loaded, the core generates a UUID token and sends it to the module process via the container's `sendToken()` mechanism (currently a Unix domain socket managed by `SubprocessContainer`). On the child side, `SubprocessTokenReceiver` receives the token before the module loader initializes the plugin. The module uses this token to authenticate calls from the core.
**Listener authentication (CWE-940).** The token socket lives at a predictable path (`$TMPDIR/logos_token_<name>[_<instanceId>]`) in a world-writable temp directory, so a local co-tenant could pre-bind that path before the legitimate child calls `listen()`. A bare `connect()` only proves *something* is listening — not that it is the child the core spawned. Before writing the token, the parent therefore authenticates the connected peer's kernel-reported credentials: it requires the peer to run as the core's own effective uid, and — when the child's pid is known (the normal load path, where `launch()` records it before `sendToken()` runs) — to be exactly that process. The check uses `SO_PEERCRED` on Linux (uid + pid) and, on macOS, `getpeereid()` for the uid plus `getsockopt(SOL_LOCAL, LOCAL_PEERPID)` for the pid — so both platforms enforce the uid + pid gate. Any mismatch is fatal: the parent refuses to write the token rather than risk leaking it to a squatter, and the send fails. **Residual risk:** when the child pid is unknown (the placeholder path used by some callers) only the uid gate applies, so a same-uid co-tenant could still receive the token; and even with the pid gate a same-uid attacker can in principle race for the path between the child's `listen()` and the parent's `connect()`. Closing that window entirely requires handing the child a pre-connected `socketpair()` fd instead of a named path, eliminating the predictable socket altogether — a planned hardening. The child/receiver side (`SubprocessTokenReceiver`) has a symmetric exposure — it accepts the connecting peer without a credential check — and needs the mirror-image hardening; the `socketpair()` redesign would close both halves at once.
2. **Module → Module**: When modules need to communicate, they request authorization from the Capability Module, which issues a token and notifies both parties. The modules then use this token for subsequent requests.
3. **Token Storage**: Each module stores tokens in a thread-safe `TokenManager` (part of the SDK). `ModuleProxy` validates tokens before dispatching method calls.
@@ -139,7 +141,7 @@ Every module ships a `metadata.json` referenced by Qt's `Q_PLUGIN_METADATA` macr
a. The `ModuleLoader` resolves the host binary (e.g. `logos_host_qt`) and builds CLI arguments (including `--transport-set` if configured)
b. The `ModuleContainer` launches the process with the resolved binary and arguments
7. Core generates a UUID authentication token
8. Core sends the token to the module via the runtime's `sendToken()` (delegates to the container)
8. Core sends the token to the module via the runtime's `sendToken()` (delegates to the container, which authenticates the receiving peer's credentials before writing the secret — see Token-Based Authentication)
9. Host process receives the token via `SubprocessTokenReceiver` (container concern), then loads the module plugin and calls `initLogos(LogosAPI*)` (loader/runtime concern)
10. The `LogosAPI` instance exposes `modulePath`, `instanceId`, and `instancePersistencePath` as properties
11. Host process registers the module with the remote object registry