fix(daemon): use hash-only Windows named-pipe name (#86)

The per-user pipe name embedded %USERNAME% verbatim
(\\.\pipe\bsk-daemon-{user}-{hash}). When the username contains
apostrophes (e.g. z'z'f'l'g'y), NPFS behaviour diverges between create
and open: the daemon's CreateNamedPipeW reports success and logs that it
is listening, yet every client CreateFileW on the same name fails with
ERROR_FILE_NOT_FOUND / ERROR_PATH_NOT_FOUND, so the CLI can never reach
the daemon (issue #75).

The name is now hash-only (\\.\pipe\bsk-daemon-{hash}); the hash
already covers user + home, so uniqueness and per-user/BSK_HOME
isolation are unchanged, and no raw username characters can reach NPFS.
The pipe name is exchanged exclusively through daemon.json (the CLI
never computes it), so old/new CLI/daemon combinations stay compatible.

Unit tests pin the safe character set for apostrophe/space/Unicode
usernames and the per-(user, home) uniqueness.

Fixes #75
This commit is contained in:
BB-fat
2026-08-12 22:10:05 +08:00
committed by GitHub
parent f443cfae74
commit 78c2719718
+53 -4
View File
@@ -103,19 +103,34 @@ pub fn record_session_path() -> Result<PathBuf> {
/// Windows named-pipe name. Include the resolved `BSK_HOME` path in the
/// token so test homes and custom installs do not share a predictable
/// per-username pipe.
///
/// The name is hash-only (`bsk-daemon-<hex>`): the hash already covers
/// user + home, so uniqueness and per-user isolation are unchanged, and
/// the pipe name never contains raw username characters. Usernames with
/// apostrophes/spaces/non-ASCII characters (e.g. `z'z'f'l'g'y`) make
/// NPFS misbehave — `CreateNamedPipeW` reports success yet clients get
/// `ERROR_FILE_NOT_FOUND` opening the very same name (issue #75).
#[cfg(windows)]
pub fn pipe_name() -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let user = env::var("USERNAME").unwrap_or_else(|_| "default".to_string());
let home = bsk_home()
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_else(|_| "unknown-home".to_string());
render_pipe_name(&user, &home)
}
/// Render the Windows named-pipe name for a user/home pair. Kept
/// platform-agnostic so unit tests on any host can pin the output
/// character set.
#[cfg(any(windows, test))]
fn render_pipe_name(user: &str, home: &str) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
user.hash(&mut hasher);
home.hash(&mut hasher);
format!(r"\\.\pipe\bsk-daemon-{user}-{:016x}", hasher.finish())
format!(r"\\.\pipe\bsk-daemon-{:016x}", hasher.finish())
}
#[cfg(test)]
@@ -174,4 +189,38 @@ mod tests {
);
});
}
/// Issue #75: usernames with apostrophes/spaces/non-ASCII characters
/// must not leak into the pipe name — NPFS misbehaves on such names
/// (CreateNamedPipeW succeeds, yet clients opening the same name get
/// ERROR_FILE_NOT_FOUND). The name must be hash-only.
#[test]
fn pipe_name_uses_safe_charset_for_special_usernames() {
for user in ["z'z'f'l'g'y", "user name", "用户", "a\"b\\c", "plain"] {
let name = render_pipe_name(user, r"C:\Users\whatever\.bsk");
let suffix = name
.strip_prefix(r"\\.\pipe\bsk-daemon-")
.unwrap_or_else(|| panic!("unexpected pipe name shape: {name}"));
assert_eq!(
suffix.len(),
16,
"pipe name {name} must carry a 16-hex-char hash"
);
assert!(
suffix.chars().all(|c| c.is_ascii_hexdigit()),
"pipe name {name} must be hash-only (did user {user:?} leak?)"
);
}
}
/// The hash-only name stays unique per (user, home) and deterministic
/// across calls (daemon and CLI exchange it via daemon.json, but a
/// stable value keeps logs/diagnostics comparable).
#[test]
fn pipe_name_is_deterministic_and_scoped() {
let a = render_pipe_name("alice", r"C:\Users\alice\.bsk");
assert_eq!(a, render_pipe_name("alice", r"C:\Users\alice\.bsk"));
assert_ne!(a, render_pipe_name("bob", r"C:\Users\alice\.bsk"));
assert_ne!(a, render_pipe_name("alice", r"D:\alt-home\.bsk"));
}
}