Files

249 lines
15 KiB
Markdown

# Row 253: private tester socket in the host scripts
Gander, 2026-10-21. F27, #1758. Base c3d88840c, after row196 landing
7247d80c7. Brief: docs/plans/2026-10-21_tester-socket-scripts.md at
0cdead66c. Reviewer Duckworth; gate owner Sage. Status: proposed, no product
code or tests changed. Claim gander-253-start is queue rev2995.
## Outcome and scope
The factory installer renders a bare --tester-socket flag instead of a TCP
port. Case1 acceptance consumes that saved form and examines the private
listener without claiming that a private pathname defeats hostile same-uid
processes. The library profile still has no tester. No .53 or .54 host may
take a copy carrying196 before253 lands, per mos-1198.
Change scripts/case2-unit.sh, scripts/case1-accept.sh and their two existing
CLI test files. Their comments/usage and fixture docs move with the code.
Do not edit the deliberate legacy paths named in the brief. No runtime bus,
Console, role, grant, control-board, dependency or systemd-template change.
No live installer or acceptance run, provider, home/config, unit or host access.
## Choices
1. Recommended: resolve the configured dataRoot as the stack user, preserve
hardening, check the final pathname's metadata and use SO_PEERCRED on a
connection to identify its listener. This supports custom roots within the
unit's existing writable home and matches196's kernel-peer approach.
2. Assume <home>/.mosaic-dev and inspect a pathname in /proc/net/unix. This
misses a custom config and is not a reliable final-path ownership check:
196 binds a temporary name and renames it. A socket's filesystem inode also
must not be confused with its kernel socket inode. Do not use this option.
3. Add a TCP relay, relax hardening or give acceptance a configurable probe
executable. All widen the surface or trust inputs and are outside this row.
There is no second bus principal verifier. The acceptance probe observes an
OS endpoint; it grants no capability and sends no HTTP or bus request.
## Installer arguments and preflight
Factory keeps required --port (1..1023) and --business; tester remains optional
for the installer. --tester-socket is a single marker, takes no value and keeps
its position in the rendered flags. The existing duplicate-flag check applies.
A following stray value is an unknown flag. --tester-port always refuses in
serve arguments, for both profiles, before unit writes or systemctl actions:
case2-unit: case2-unit.sh retires tester TCP; use --tester-socket
Library rejects --tester-socket with an explicit library-profile refusal. Its
other flags, proof-file checks and install behavior are unchanged. No legacy
conversion or fallback to TCP is introduced in this installer.
When the factory socket marker is present, a small inline metadata query runs
the selected copy's config loader under the target uid with sudo -u USER -H,
env -i, explicit HOME and the same pinned-node-first PATH used by the unit.
Root never imports user-writable copy code as root. It invokes the previously
validated --node path after dropping uid, with the copy module path as a
separate argument. This resolves the default instance just as the rendered
unit does; no MOSAIC_CONFIG or named-instance override is inherited.
The query returns only dataRoot and whether that copy's Console carries the
MOSAIC_TESTER_SOCKET_V1 marker. It prints no config values, credentials or child
diagnostics. Bound it to five seconds and 4096 output bytes; reject errors,
extra output/keys, malformed results, missing config and unsupported targets.
No root process opens a config or credential under the user's home. Existing
root metadata checks remain root metadata checks. Both script headers explain
the new, explicitly unprivileged config query.
Before any unit writes, validate the socket location:
- dataRoot is an absolute normalized plain path, below the declared home,
with no symlink in its existing path. Never expand the unit's writable scope.
- Neither home nor dataRoot is under /home, /root, /run/user, /tmp or /var/tmp.
These roots conflict with the existing ProtectHome/PrivateTmp contract.
- <dataRoot>/console/tester.sock is at most 107 ASCII bytes, matching196.
- A missing console parent is allowed; runtime creates it. An existing parent
must be a real directory, not a symlink, owned by the target uid with no
group/other permissions. A foreign owner, non-directory or unsafe mode refuses.
- Do not create/chmod the parent or inspect/delete an existing tester entry in
the installer. A running Console may own it during a re-pin. Runtime196 owns
binding and cleanup. Reinstall still does not restart running units.
The dataRoot query and all refusal families get fake-only tests. A failed
preflight leaves both units and the systemctl log unchanged.
## How the rendered unit reaches the socket
The unit still has User/Group for the stack account, ProtectSystem=strict,
ReadWritePaths=<home>, ProtectHome=yes and PrivateTmp=yes, in the same order.
Its ExecStart contains --tester-socket, with no pathname argument. The Console
loads dataRoot and derives console/tester.sock itself. Case1/2's /srv/mosaic
home is outside ProtectHome's protected hierarchies; the allowed dataRoot is
inside the existing writable home, and the socket is outside private /tmp.
No extra capability or writable directory is added. Systemd's documented
namespace rules motivate the location refusals, rather than a hardening change.
[systemd.exec](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html)
A mutable same-uid config can change between preflight and start. The unit's
writable boundary is still home; runtime's own config/socket checks still
refuse unsafe state. This row does not promise an immutable same-uid config.
The host window must verify the rendered unit actually starts under its
namespace; scratch tests inspect its directives, not a real systemd namespace.
## Acceptance arguments and endpoint observation
The saved args must contain one --tester-socket and the numeric --port.
Reject a missing/duplicate marker, a marker with an old numeric value, and
--tester-port with the same migration wording naming case1-accept.sh. Preserve
all existing pre-- argument validation and the default-instance contract.
Malformed saved inputs still refuse with exit2 before functional checks.
Acceptance resolves the actual configured tester dataRoot with the same
unprivileged, bounded query. It does not silently use the default path. An
unreadable/invalid config or a root outside the unit's allowed home refuses
before the functional talk. The old DATA_ROOT used by F11 remains unchanged;
generalizing that separate Discord check is outside this row.
P10 independently requires the Console's systemd MainPID to be positive and
stable, and its four /proc uid fields to be the stack uid. Check the private
parent's type, owner and mode, and the final entry's non-symlink socket type,
owner and exact 0600 mode. Snapshot parent/socket dev+ino and metadata before
and after the peer probe; a replacement or MainPID change fails observation.
The port floor must still be numeric and above the privileged main Console
port, retaining that existing part of the observation.
The bounded probe runs as the stack user with an empty inherited environment,
the fixed system /usr/bin/python3 resolved to a root-owned ELF, safe root-owned
ancestors, and -I -S. This is the system Python dependency already required by
196; never use PATH python, a home shim or a configurable helper. Unsafe/missing
Python is a failed P10 observation, not a fallback. Resolve/check the system
entry and target using metadata and the ELF header; do not run copy Python code
as root. Test seams emulate these root facts and the system executable.
The probe opens AF_UNIX/SOCK_STREAM, uses a one-second connect timeout, reads
SO_PEERCRED and closes. An outer three-second process-group bound applies. It
returns only pid/uid/gid, with exact structure, bounded output and numeric
validation. pid must equal MainPID and uid the stack uid; gid is validated but
not an identity grant. No application bytes, cookies, credentials or proof
envelope are sent. Linux supplies the peer credentials for the connected
stream, so the check does not depend on the temporary bind pathname.
[unix(7), SO_PEERCRED](https://man7.org/linux/man-pages/man7/unix.7.html)
This is a snapshot. Hostile same-uid processes can alter same-uid state or
interfere with the unprivileged probe. Root, ptrace and later pathname changes
are not defeated here. The probe does not prove another uid is denied; that
remains a host-window observation, not a fake test claim.
## P8 and P10 outcome lines
P8 must change with removal of TESTER_PORT. It checks the main Console port is
loopback-only and held only by MainPID, and that nothing listens on TCP7331 or
the retired TCP7333. Socket ownership belongs to P10. Its healthy line is:
P8 lan-listeners closed <port> is on 127.0.0.1 only by MainPID <pid>; no TCP listener on 7331 or 7333; tester uses <socket>
An unexpected listener, missing main listener or wrong owner produces P8
lan-listeners unnamed with the observed listener summaries, as today.
Do not reuse accepted mos-1101 for a new Unix-socket claim: that ruling named a
TCP-port condition. Proposed safe default pending any new Mos classification:
P10 tester-socket-squat unnamed boundary=observed path=<socket> parent=0700 owner=<uid> socket=0600 peer_pid=<pid> peer_uid=<uid> floor=<floor> port=<port>; hostile same-uid substitution remains
Any missing/unsafe metadata, wrong peer, unsafe interpreter, malformed/timeout
probe, floor miss or changed snapshot produces one P10 line instead:
P10 tester-socket-squat unnamed boundary=failed reason=<bounded-code> path=<socket> MainPID=<pid>; hostile same-uid substitution remains
Healthy observation does not mark the same-uid squat path closed or invent an
accepted ruling. P checks still never change exit status. Tests pin the reason
and observed/failed distinction, so validation cannot disappear behind an
always-unnamed mark. Sage/Mos may supply a new classification at design review;
it must be explicit before changing this proposed mark. No Jason question is
needed merely to retain unnamed evidence.
## Evidence and planned checks
Keep the existing scratch fixtures, fake account database/systemctl/curl/ss/
sudo/stat and fake interpreter. Add copy config/marker fixtures and probe
metadata under those same roots. No test connects to a live socket or runs the
real host scripts without their existing seams. Fixture fakes verify the
unprivileged command argv; no test claims a real uid drop or namespace.
The fake system interpreter executes the actual probe payload against a fake
Python socket API, checking AF_UNIX/SOCK_STREAM, timeout, connect, getsockopt
arguments and returned peer bytes. This catches errors in the inline probe
without opening an OS listener. The metadata query is tested with nonzero
status plus valid-looking output as well as malformed output, so removing its
status refusal cannot hide behind a later parse error.
| Checks | Required observations |
|---|---|
| T1 factory render | Bare marker exactly once, argument order retained, no TCP flag, hardening unchanged; duplicate/stray value refuses |
| T2 profile/migration | Both profiles refuse retired TCP; library refuses socket; all refuse before writes/start |
| T3 config/target | Invalid/missing/noisy/timed-out metadata and unmarked target refuse; query uses target uid and clean environment |
| T4 location | Default and custom in-home roots work; outside-home, protected/private hierarchies, symlinks and >107-byte pathname refuse |
| T5 parent | Missing parent allowed; file/symlink/foreign owner/group-other mode refuse without mutation or systemctl calls |
| T6 saved args | Required socket marker; missing/duplicate/stray value and retired TCP refuse exit2 before talk |
| T7 P8 | Main-only loopback succeeds; LAN/wrong owner/7331 or7333 listener gives unnamed |
| T8 P10 | Exact metadata, stable MainPID and matching peer produce boundary=observed; custom configured root used |
| T9 failed P10 | Parent/socket faults, wrong PID/uid, malformed/timeout probe, unsafe Python, bad floor and changed snapshot each give failed reason |
| T10 preserved behavior | Functional checks, their exit semantics, library behavior and all other P paths retained |
Planned checks to kill, one target change at a time in isolated snapshots:
| IDs | Removed requirement |
|---|---|
| M1-M3 | Installer TCP refusal, library socket refusal, duplicate socket refusal |
| M4-M5 | Config-query failure refusal, unsupported-target refusal |
| M6-M8 | Home containment, protected/private path refusal,107-byte limit |
| M9-M11 | Existing parent type/symlink, uid, mode checks |
| M12-M15 | Acceptance TCP refusal, required marker, duplicate/stray marker rejection, config-query refusal |
| M16 | P8 retired TCP7333 check |
| M17-M18 | P10 parent and final socket metadata checks |
| M19-M21 | Peer PID, peer uid, malformed/timeout probe refusal |
| M22-M23 | Safe fixed system interpreter, stable metadata/MainPID snapshot |
| M24-M25 | Target-uid invocation in installer and acceptance metadata/probe paths |
| M26 | Privileged main-port floor observation |
| M27 | Probe returns its own PID rather than the connected peer's credentials |
Before execution, give every definition one unique source target and a named
killer test. Preserve raw logs, base times, syntax preflight and byte-identical
SHA/Git restoration. A timeout counts only under Sage's explicit rules and
must kill the process group. Refusal variants in T1-T9 must not be merely
assertions of a generic nonzero exit.
## Static inventory and gate
After implementation, enumerate every --tester-port hit in scripts/packages
in BUILD.md and a machine-readable file. The only permitted hits are the brief's
deliberate legacy producers/parsers/tests and this row's explicit refusals and
negative fixtures. Inspect arguments and generated units, not just filenames.
Readmes must describe retirement rather than produce old argv. No modification
to webui-service, stack-release, instance, their tests/fixture, case1-hostcheck
or tester-cli negative calls is planned. Stop and ask Sage if one is needed.
Then request Sage's slot. Under that grant, run CLI whole, WebUI whole, every
shell test suite whole and all named checks above. Use an isolated candidate,
plain linked node_modules, a short gXXX TMPDIR under /mnt/storage/scratch/tmp,
busy &&, pipefail and tee. Single/pattern runs need the slot too, per sage-2142.
No harness/helper/log argument may match the busy detector pattern, per
mos-1205. Two failures end the window; report any stop to Sage/Mos before a
retry. Return the slot with script-summed counts/timings and a leftover sweep.
Design approval precedes code. The manifest, immutable candidate hashes,
static inventory and gate evidence go to Duckworth before Sage's landing.
Queue moves use the helper, with a coordinated queue-write gap. Sage posts the
jarvis review request after the in-review move. BUILD-LOG/session history stays
append-only; Sage continues session registration. No host deployment is part
of row253 acceptance.