Skip to content

Commit a031530

Browse files
NathanFlurryclaude
andcommitted
feat: US-072 - Fix auth token comparison and socket directory security
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 76e0fb1 commit a031530

1 file changed

Lines changed: 51 additions & 5 deletions

File tree

crates/v8-runtime/src/main.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ mod snapshot;
1414
use std::collections::HashMap;
1515
use std::fs;
1616
use std::io::{self, Read, Write};
17-
use std::os::unix::fs::PermissionsExt;
17+
use std::os::unix::fs::DirBuilderExt;
1818
use std::os::unix::io::{AsRawFd, RawFd};
1919
use std::os::unix::net::{UnixListener, UnixStream};
2020
use std::path::PathBuf;
@@ -90,12 +90,13 @@ fn random_hex_128() -> io::Result<String> {
9090
Ok(buf.iter().map(|b| format!("{:02x}", b)).collect())
9191
}
9292

93-
/// Create a secure tmpdir with 0700 permissions and return the socket path inside it
93+
/// Create a secure tmpdir with 0700 permissions and return the socket path inside it.
94+
/// Uses DirBuilder::mode() to set permissions atomically via mkdir(2), avoiding
95+
/// a TOCTOU race between create_dir and set_permissions.
9496
fn create_socket_dir() -> io::Result<(PathBuf, PathBuf)> {
9597
let suffix = random_hex_128()?;
9698
let tmpdir = std::env::temp_dir().join(format!("secure-exec-{}", suffix));
97-
fs::create_dir(&tmpdir)?;
98-
fs::set_permissions(&tmpdir, fs::Permissions::from_mode(0o700))?;
99+
fs::DirBuilder::new().mode(0o700).create(&tmpdir)?;
99100
let socket_path = tmpdir.join("secure-exec.sock");
100101
Ok((tmpdir, socket_path))
101102
}
@@ -106,13 +107,26 @@ fn cleanup(socket_path: &PathBuf, tmpdir: &PathBuf) {
106107
let _ = fs::remove_dir(tmpdir);
107108
}
108109

110+
/// Constant-time byte comparison to prevent timing oracle on auth token.
111+
/// Returns true if both slices have equal length and identical contents.
112+
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
113+
if a.len() != b.len() {
114+
return false;
115+
}
116+
let mut diff = 0u8;
117+
for (x, y) in a.iter().zip(b.iter()) {
118+
diff |= x ^ y;
119+
}
120+
diff == 0
121+
}
122+
109123
/// Authenticate a new connection by reading the first message as an Authenticate token.
110124
/// Returns true if authentication succeeds, false otherwise.
111125
fn authenticate_connection(stream: &mut UnixStream, expected_token: &str) -> bool {
112126
// Connection is blocking — read the first message
113127
match ipc_binary::read_frame(stream) {
114128
Ok(BinaryFrame::Authenticate { token }) => {
115-
if token == expected_token {
129+
if constant_time_eq(token.as_bytes(), expected_token.as_bytes()) {
116130
true
117131
} else {
118132
eprintln!("auth failed: invalid token");
@@ -425,6 +439,7 @@ fn main() {
425439
#[cfg(test)]
426440
mod tests {
427441
use super::*;
442+
use std::os::unix::fs::PermissionsExt;
428443
use std::os::unix::io::AsRawFd;
429444
use std::os::unix::net::UnixStream;
430445

@@ -659,6 +674,37 @@ mod tests {
659674
cleanup(&socket_path, &tmpdir);
660675
}
661676

677+
#[test]
678+
fn constant_time_eq_matches_equal_strings() {
679+
assert!(constant_time_eq(b"hello", b"hello"));
680+
assert!(constant_time_eq(b"", b""));
681+
assert!(constant_time_eq(b"abc123xyz", b"abc123xyz"));
682+
}
683+
684+
#[test]
685+
fn constant_time_eq_rejects_different_strings() {
686+
assert!(!constant_time_eq(b"hello", b"world"));
687+
assert!(!constant_time_eq(b"hello", b"hellx"));
688+
// Single-bit difference
689+
assert!(!constant_time_eq(b"\x00", b"\x01"));
690+
}
691+
692+
#[test]
693+
fn constant_time_eq_rejects_different_lengths() {
694+
assert!(!constant_time_eq(b"hello", b"hell"));
695+
assert!(!constant_time_eq(b"", b"x"));
696+
assert!(!constant_time_eq(b"abc", b"abcd"));
697+
}
698+
699+
#[test]
700+
fn socket_dir_has_0700_permissions() {
701+
let (tmpdir, socket_path) = create_socket_dir().expect("create socket dir");
702+
let meta = fs::metadata(&tmpdir).expect("stat tmpdir");
703+
let mode = meta.permissions().mode() & 0o777;
704+
assert_eq!(mode, 0o700, "socket dir should have 0700 permissions, got {:o}", mode);
705+
cleanup(&socket_path, &tmpdir);
706+
}
707+
662708
#[test]
663709
fn poll_wakes_on_self_pipe_not_listener() {
664710
let (listener, socket_path, tmpdir) = temp_listener();

0 commit comments

Comments
 (0)