249 lines
15 KiB
Markdown
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.
|