@@ -14,7 +14,7 @@ mod snapshot;
1414use std:: collections:: HashMap ;
1515use std:: fs;
1616use std:: io:: { self , Read , Write } ;
17- use std:: os:: unix:: fs:: PermissionsExt ;
17+ use std:: os:: unix:: fs:: DirBuilderExt ;
1818use std:: os:: unix:: io:: { AsRawFd , RawFd } ;
1919use std:: os:: unix:: net:: { UnixListener , UnixStream } ;
2020use 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.
9496fn 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.
111125fn 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) ]
426440mod 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