docs: finalize v1 design and Windows service model
This commit is contained in:
+195
-29
@@ -4,6 +4,10 @@ Daemon configuration uses strict TOML as specified in
|
||||
[`configuration.md`](configuration.md); the annotated examples contain every
|
||||
v1 knob and default.
|
||||
|
||||
The implementation order is Windows client first, Linux client second. The
|
||||
sections below describe both final contracts; their document order does not
|
||||
override that release gate.
|
||||
|
||||
## Unix-like clients
|
||||
|
||||
The client starts `sh` or `bash` in a new session/process group. Unix signals
|
||||
@@ -65,52 +69,210 @@ supervisory cgroup imposes no resource limit.
|
||||
## Windows clients
|
||||
|
||||
The minimum supported v1 Windows versions are Windows 10 and Windows Server
|
||||
2016. For every command, create a non-inheritable Job Object, set
|
||||
`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, and do not enable breakaway. The daemon
|
||||
starts an RVBox per-command launcher suspended with `CREATE_NEW_CONSOLE`,
|
||||
`CREATE_UNICODE_ENVIRONMENT`, and `EXTENDED_STARTUPINFO_PRESENT`; it assigns the
|
||||
launcher atomically through `PROC_THREAD_ATTRIBUTE_JOB_LIST`. Only an explicit
|
||||
standard-I/O and launcher-control handle list is inherited, and the Job handle
|
||||
is never inherited.
|
||||
2016. Desktop Experience is required only for the tray and active-session
|
||||
execution contexts; the service and Session 0 contexts may run headless on
|
||||
Server Core. Installation is an explicit UAC-elevated operation that registers
|
||||
one machine-wide Automatic SCM service. The service runs as `LocalSystem` before
|
||||
login and alone owns configuration, WSS, durable state, logs, admission,
|
||||
history, process handles, and Job Objects. Normal service startup never shows
|
||||
UAC and is not blocked by tray or interactive-user availability.
|
||||
|
||||
The launcher invokes exactly the selected shell against the generated wrapper:
|
||||
The same signed `rvbox.exe` has explicit `service`, `tray`, `install-service`,
|
||||
`uninstall-service`, `configure-service`, per-command launcher, and signal-
|
||||
helper modes. Internal modes require SCM state or a service-created launch
|
||||
proof. Task Scheduler is not used. The installer registers an unelevated per-
|
||||
user tray launch through the machine-wide `Run` key. One tray may run in each
|
||||
logged-in session; its exit, failure, disablement, or Explorer restart does not
|
||||
stop the service or commands.
|
||||
|
||||
The executable uses the Windows GUI subsystem so service, tray, and internal
|
||||
helpers do not flash consoles. Human-invoked console modes such as
|
||||
`--check-config`, install, uninstall, and service configuration first call
|
||||
`AttachConsole(ATTACH_PARENT_PROCESS)`, rebuild the standard handles, and emit
|
||||
normal UTF-8 diagnostics when a parent console exists; otherwise they use a
|
||||
native dialog or documented exit code. Internal launcher arguments are never
|
||||
printed. This preserves usable terminal help/errors without adding a second
|
||||
persistent executable or process.
|
||||
|
||||
The tray is a thin frontend over a local-only named pipe. It never opens the
|
||||
SQLite store/spool or owns a server connection. The pipe rejects remote clients,
|
||||
has explicit ACLs, and the service impersonates each caller for authorization.
|
||||
Read-only health/status is available to an interactive local user. Service
|
||||
start/stop/restart, Automatic/Manual startup changes, config editing, incident
|
||||
resolution, and other machine-wide mutations require a locally elevated
|
||||
administrator helper. `Exit` closes only that tray. Server Core and logged-out
|
||||
machines simply have no tray.
|
||||
|
||||
When no config path was installed, resolve `FOLDERID_ProgramData` with the
|
||||
Windows Known Folder API and use `RVBox\client.toml`, `RVBox\state`,
|
||||
`RVBox\work`, and `RVBox\logs\rvbox.log` beneath it. Do not expand
|
||||
`%ProgramData%` text from TOML. State and the work root are writable only by
|
||||
SYSTEM and Administrators. When `cwd` is omitted, the service creates or opens
|
||||
an identity-scoped child beneath the work root: a user-SID directory for either
|
||||
active-user context, a LocalService directory, or a SYSTEM-only directory for
|
||||
each SYSTEM context. Its non-inherited ACL grants only SYSTEM and the effective
|
||||
non-SYSTEM SID the required access. The service validates the existing owner,
|
||||
ACL, and reparse-point state before reuse, so one user cannot pre-create or
|
||||
modify another identity's work area. Config/log read access needed by the tray
|
||||
is separate from edit access. First installation writes the annotated template
|
||||
and lets the service remain live but not ready until routing validates.
|
||||
Configuration is restart-only. File opening uses exact resolved regular paths,
|
||||
never a constructed shell command.
|
||||
|
||||
### Windows execution-context hierarchy
|
||||
|
||||
`ExecutionSpec.elevated` is the only caller-facing privilege choice. Windows
|
||||
combines it with the presence of one usable active interactive session to select
|
||||
an effective context. The ordered rules are:
|
||||
|
||||
| Login state | `elevated` | Ordered pre-launch contexts |
|
||||
| --- | --- | --- |
|
||||
| usable active user | false | `active_user` only |
|
||||
| usable active user | true | `active_user_elevated` -> `active_system` -> `local_system` |
|
||||
| no usable active user | false | `local_service` only |
|
||||
| no usable active user | true | `local_system` only |
|
||||
|
||||
Here `active_user` is the selected user's deliberately non-elevated token;
|
||||
`active_user_elevated` is that user's traditional full administrator token;
|
||||
`active_system` is SYSTEM placed in the selected session; and the two `local_*`
|
||||
contexts run in Session 0. “Usable” means the deterministic WTS selection and
|
||||
token validation below succeeded, not merely that some disconnected session
|
||||
record exists.
|
||||
|
||||
Fallback is allowed only during token/session selection before
|
||||
`launch_prepared`. Failure to create a launcher/shell, an uncertain launch, CWD
|
||||
or executable rejection, or failure after authorization never tries another
|
||||
identity. The persisted attempt list and selection detail state why an elevated
|
||||
request reached SYSTEM. If a formerly active user logs out during selection,
|
||||
the service re-enumerates once and applies the no-user row; it never retargets a
|
||||
different user silently.
|
||||
|
||||
The LocalSystem service obtains `local_service` with passwordless
|
||||
`LogonUserW("LocalService", "NT AUTHORITY", NULL, LOGON32_LOGON_SERVICE, ...)`.
|
||||
`local_system` duplicates the service token. To find an active user, enumerate
|
||||
WTS sessions and obtain the selected token with `WTSQueryUserToken`. Prefer a
|
||||
valid active physical-console session; if none exists, accept exactly one
|
||||
`WTSActive` interactive session. Multiple remaining candidates are ambiguous,
|
||||
so there is no usable active user rather than a nondeterministic choice.
|
||||
Immediately before release, require the same session ID, logon SID, and user SID.
|
||||
|
||||
For `active_user_elevated`, inspect `TokenElevationType`. A traditional limited
|
||||
administrator token must expose a linked full token; an already-full
|
||||
administrator token is usable as-is. A standard user returns
|
||||
a `CODE_ELEVATION_UNAVAILABLE` attempt reason. Windows Administrator Protection, or
|
||||
another policy requiring interactive approval rather than exposing a reusable
|
||||
full token, records a bounded policy-specific attempt reason. V1 never waits
|
||||
for a UAC/Hello prompt; an elevated request proceeds to `active_system`.
|
||||
That context duplicates the service token and sets its `TokenSessionId`
|
||||
explicitly. If session-scoped SYSTEM construction also fails before preparation,
|
||||
the final context is Session 0 `local_system`. This fallback intentionally
|
||||
bypasses user-scoped approval because the installed service already holds
|
||||
SYSTEM; it must be conspicuous in status/audit history. The final
|
||||
`local_system` fallback supplies elevation but not access to the interactive
|
||||
desktop: a display, audio, or other session-scoped command may therefore fail
|
||||
normally in Session 0. Selection fallback is not a promise that the requested
|
||||
operation is meaningful in the resulting context.
|
||||
|
||||
For normal `active_user`, use the filtered token when Windows supplies one. If
|
||||
an administrator is logged in with only a full token (for example traditional
|
||||
UAC is disabled), create and verify a LUA-style restricted medium-integrity
|
||||
token with administrator SIDs deny-only and unnecessary privileges removed.
|
||||
Never let `elevated=false` inherit a full administrator token merely because of
|
||||
machine policy; if a verified non-elevated token cannot be built, reject rather
|
||||
than use SYSTEM or LocalService while that active session remains selected.
|
||||
|
||||
Build the base environment from the effective token and then apply persisted
|
||||
overrides. Session 0 contexts do not inherit a person's profile, mapped drives,
|
||||
user certificates, or per-user proxy settings. LocalService may use ordinary
|
||||
DNS/TCP/HTTP(S), localhost sockets, and ACL-permitted local files, but presents
|
||||
anonymous credentials to remote Windows resources. Active-user contexts load
|
||||
that user's profile/environment as needed and unload only after the whole Job
|
||||
exits. `active_system` remains SYSTEM: putting it in the user's session does not
|
||||
give it that user's HKCU, profile, mapped drives, or network credentials.
|
||||
|
||||
### Windows process launch and supervision
|
||||
|
||||
For every command, create a non-inheritable Job Object, set
|
||||
`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, and do not enable breakaway. Use a short-
|
||||
lived stateless per-command launcher mode of the same executable for every
|
||||
context. This uniform path is required because Windows does not permit normal
|
||||
inherited handles across Terminal Services sessions. The launcher owns no
|
||||
durable state or policy and remains inside the command Job only to bridge
|
||||
standard I/O and watch its shell.
|
||||
|
||||
The service creates local named pipes for launcher control and stdin/stdout/
|
||||
stderr using unpredictable per-command names, `PIPE_REJECT_REMOTE_CLIENTS`, and
|
||||
ACLs limited to SYSTEM plus the selected token SID. It creates the launcher
|
||||
suspended with `CreateProcessAsUser`, no inherited handles, the selected token/
|
||||
session, and an opaque channel identifier. Before resuming it, the service
|
||||
assigns the launcher to the empty Job. On connection, verify the named-pipe
|
||||
client PID, its `GetProcessTimes` creation `FILETIME`, expected token SID,
|
||||
session ID, and launch generation; a same-user process racing for a pipe cannot
|
||||
satisfy that complete identity.
|
||||
|
||||
The launcher opens only those pipes, creates the selected shell suspended in
|
||||
its own dedicated console with `CREATE_NEW_CONSOLE`, `CREATE_SUSPENDED`,
|
||||
`CREATE_UNICODE_ENVIRONMENT`, and `EXTENDED_STARTUPINFO_PRESENT`, and uses
|
||||
`STARTF_USESHOWWINDOW`/`SW_HIDE`. It inherits only the explicit standard-I/O
|
||||
handles through `PROC_THREAD_ATTRIBUTE_HANDLE_LIST`. Non-breakaway Job
|
||||
membership propagates from the launcher. It reports the suspended shell PID and
|
||||
creation `FILETIME` and waits for the service's release/watchdog channel.
|
||||
|
||||
Invoke exactly the selected shell against the generated wrapper:
|
||||
`cmd.exe /D /S /C` for a `.cmd` wrapper, or `powershell.exe` with `-NoLogo`,
|
||||
`-NoProfile`, `-NonInteractive`, and `-File` for a `.ps1` wrapper. Application
|
||||
paths and argument quoting are constructed by the Windows launcher, never by
|
||||
concatenating an untrusted command line. There is no fallback between shells.
|
||||
paths and argument quoting are constructed by one reviewed Windows routine,
|
||||
never by concatenating an untrusted command line. There is no fallback.
|
||||
|
||||
Persist and flush `launch_prepared` with the launcher PID,
|
||||
`GetProcessTimes` creation `FILETIME`, and launch generation. Persist and flush
|
||||
`launch_authorized` before calling `ResumeThread`. The launcher then starts the
|
||||
requested `cmd` or `powershell` suspended in its console with
|
||||
`CREATE_NEW_PROCESS_GROUP`, reports the shell PID/group through the private
|
||||
control channel, connects the allowlisted pipes, and resumes it. This two-step
|
||||
shape is required because `CREATE_NEW_PROCESS_GROUP` is ignored when combined
|
||||
with `CREATE_NEW_CONSOLE`, and console control events reach only groups sharing
|
||||
the caller's console. Failure after authorization terminates the Job and is
|
||||
reported as interrupted; it never redispatches the UUID.
|
||||
|
||||
The launcher remains the in-console signal proxy and calls
|
||||
`GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, shell_group_id)` on request. The
|
||||
daemon retains the sole Job handle, so an unclean daemon exit closes the last
|
||||
handle and terminates the launcher, shell, and descendants. Recovery never
|
||||
kills by persisted PID alone; the PID/creation-time tuple is diagnostic evidence
|
||||
for PID reuse or cleanup anomalies. Child processes normally join the Job.
|
||||
Persist and flush `launch_prepared` with requested elevation, attempted/effective
|
||||
context, selection detail, effective token SID, selected session/user SID where
|
||||
applicable, launcher and shell PID/creation times, and launch generation while
|
||||
the shell remains suspended.
|
||||
Persist and flush `launch_authorized` before sending the release token that
|
||||
makes the launcher call `ResumeThread`. Control-channel EOF before authorization
|
||||
exits without running user code; EOF afterward terminates the Job. A failure
|
||||
after authorization is interrupted and never redispatched. The service retains
|
||||
the sole Job handle, so an unclean service exit terminates launcher, suspended
|
||||
or running shell, and descendants. Recovery never kills by persisted PID alone.
|
||||
Job Object limits enforce requested profiles and `KILL_ON_JOB_CLOSE` protects
|
||||
against lost supervision.
|
||||
|
||||
Do not request `CREATE_NEW_PROCESS_GROUP` with `CREATE_NEW_CONSOLE`; Windows
|
||||
ignores that combination. TERM instead launches a short-lived private mode of
|
||||
the same `rvbox.exe` under the command's effective token and session. It uses a
|
||||
separate PID-verified local named-pipe handshake, not inherited cross-session
|
||||
handles, to receive the target PID/creation-time/generation. It attaches to the
|
||||
command's dedicated console, installs a handler that consumes its own
|
||||
CTRL_BREAK, and calls
|
||||
`GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, 0)`. Prefer the live root PID; after
|
||||
root exit, select and birth-verify a live PID from the Job list. Because each
|
||||
command owns its console, this does not address an unrelated command. The
|
||||
helper detaches and reports delivery; it receives neither the Job handle nor
|
||||
command stdio.
|
||||
|
||||
Root-process exit begins the same drain grace period. Completion waits for the
|
||||
Job Object to reach zero active processes; after the grace period RVBox
|
||||
terminates the Job, drains its capture handles, records any incomplete-output
|
||||
marker, and emits the terminal lifecycle event last.
|
||||
|
||||
Only `TERM`/`SIGTERM` and `KILL`/`SIGKILL` are accepted. `TERM` attempts `CTRL_BREAK_EVENT`
|
||||
and waits 10 seconds, then calls Job Object termination if the job persists;
|
||||
Only `TERM`/`SIGTERM` and `KILL`/`SIGKILL` are accepted. `TERM` attempts the
|
||||
console-helper `CTRL_BREAK_EVENT` and waits 10 seconds, then calls Job Object
|
||||
termination if the job persists;
|
||||
`SIGKILL` calls Job Object termination immediately. A console signal is
|
||||
best-effort, so callers receive an explicit escalation result. Windows status
|
||||
uses process and Job Object accounting APIs; it does not claim Linux-only
|
||||
diagnostics such as an I/O wait channel.
|
||||
|
||||
Display topology APIs that require the console desktop, DDC/CI monitor
|
||||
enumeration, and active-user audio policy normally require an active context;
|
||||
SYSTEM privilege in Session 0 does not substitute for session visibility.
|
||||
Localhost TCP is machine-wide and normally works in every context, subject to
|
||||
the listener's own authentication. Fixed local-drive access follows NTFS ACLs;
|
||||
the client never broadens a requested directory automatically. Every command
|
||||
record and `stat` view includes requested elevation, attempted/effective
|
||||
contexts, selection detail, effective token SID, and target session ID/user SID
|
||||
when present.
|
||||
|
||||
## Storage and recovery
|
||||
|
||||
SQLite runs in WAL mode. Every append-only segment has a SQLite-owned
|
||||
@@ -186,6 +348,10 @@ crossing one rejects new unreserved allocations even if the logical quota has
|
||||
headroom. Already-reserved terminal/loss closeout remains writable while bytes
|
||||
physically remain.
|
||||
|
||||
The Windows service installs with Automatic startup. Its bounded file logging
|
||||
defaults to a 10 MiB current file and five retained sealed files under the
|
||||
resolved ProgramData log directory; tray lifecycle does not affect log output.
|
||||
|
||||
These bounds protect RVBox's own loops; they cannot make arbitrary child
|
||||
commands harmless when no resource profile is requested. Operators should
|
||||
enable resource profiles for untrusted or expensive workloads and keep nginx,
|
||||
|
||||
Reference in New Issue
Block a user