Windows steps 3 of D-62. Every `cfg(unix)` gate on the host, the bridge, the opener, the books server and the command line is gone: they build on every OS, and the Unix behaviour is unchanged (the existing over-the-wire suites pass on Linux). `ledger_mcp::ipc` is the one seam for the channels. Unix: the same 0600 sockets as before, peer uid unchanged (the peer functions moved here). Windows: a named pipe `\\.\pipe\wanderbooks-<hash of the root's run/>-<name>` made with a DACL of one allow entry for this account's SID, remote clients refused, the first instance made once (a second server is refused, as a held lock refuses it on Unix), and 16 idle instances kept waiting so a client never finds all busy while the server makes the next. Where Unix reads `SO_PEERCRED`, each end of a pipe takes the other's process id (`GetNamedPipeClientProcessId`/`...ServerProcessId`) and compares that process's token user with its own SID; any doubt is "not us". Clients open with SECURITY_IDENTIFICATION so a pipe server cannot act as the client. A blocking client with a read limit is polled with PeekNamedPipe (a pipe read cannot be cut short), and a pipe that has gone reads as the end of the stream, as a closed socket does. Stop signals are Ctrl+C, Ctrl+Break, console close and shutdown where Unix has SIGTERM, SIGINT and SIGHUP. The host starts detached (DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP, leaving the opener's job when the job lets it: an agent's job must not take the host down, as an app's systemd scope must not on Linux) with a fixed environment; `close_range` and `seccomp` stay Linux-only (D-32). A replaced program is told by size and last write. Git for Windows is found and a quiet git is stopped with its tree (`taskkill /T`). The test harness of ledger-cli finds its root by `default_root`, cleans a child's environment per OS and reaches the server over its pipe; the bridge's scripted-server tests stay Unix-only. No new dependency (tokio's named pipes and reqwest's `windows_named_pipe` were already in the build); `windows-sys` stays in ledger-store. Tests: ledger-mcp `ipc::tests` (four on every OS, one Windows only), ledger-host `tests/windows_pipe.rs::a_second_user_cannot_open_the_pipe`, Windows variants of the host and start tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
54 lines
1.9 KiB
Rust
54 lines
1.9 KiB
Rust
//! The books' named pipes on Windows (P11c, D-62, plan 3.9): what keeps a
|
|
//! second account out of them, in the two ways the design has (the pipe's
|
|
//! DACL, and the SID check of the process at the other end). Windows job
|
|
//! only.
|
|
#![cfg(windows)]
|
|
|
|
use ledger_mcp::ipc::{Endpoint, Listener};
|
|
use ledger_store::winsec;
|
|
use std::path::Path;
|
|
|
|
/// The process id of `image` (a system program), if `tasklist` finds it.
|
|
fn pid_of(image: &str) -> Option<u32> {
|
|
let out = std::process::Command::new("tasklist")
|
|
.args(["/FI", &format!("IMAGENAME eq {image}"), "/FO", "CSV", "/NH"])
|
|
.output()
|
|
.ok()?;
|
|
let text = String::from_utf8_lossy(&out.stdout);
|
|
// "services.exe","700","Services","0","12,345 K"
|
|
text.lines()
|
|
.find(|l| l.to_lowercase().contains(&image.to_lowercase()))?
|
|
.split(',')
|
|
.nth(1)?
|
|
.trim_matches('"')
|
|
.parse()
|
|
.ok()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_second_user_cannot_open_the_pipe() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let endpoint = Endpoint::new(dir.path(), "lease.sock").unwrap();
|
|
let _listener = Listener::bind(&endpoint).unwrap();
|
|
|
|
// The pipe's own DACL names this account and no one else: Everyone,
|
|
// Users and Authenticated Users are not on it, so the system itself
|
|
// refuses another account's open.
|
|
let pipe = Path::new(endpoint.pipe_name());
|
|
assert!(
|
|
winsec::is_private(pipe).unwrap(),
|
|
"the pipe is open to more than this account"
|
|
);
|
|
|
|
// And a process of another account is never taken for this one: this
|
|
// account's own is, and a system service (SYSTEM) is not.
|
|
assert!(winsec::pid_is_us(std::process::id()));
|
|
if let Some(pid) = pid_of("services.exe") {
|
|
assert!(
|
|
!winsec::pid_is_us(pid),
|
|
"a SYSTEM process ({pid}) was taken for this account"
|
|
);
|
|
}
|
|
assert!(!winsec::pid_is_us(0));
|
|
}
|