@@ -56,15 +56,42 @@ impl IpcServer {
5656 /// Start the IPC server. This blocks the calling thread.
5757 #[ cfg( unix) ]
5858 pub fn start ( & self ) -> Result < ( ) , String > {
59- // Clean up stale socket
60- let _ = std:: fs:: remove_file ( & self . socket_path ) ;
61-
62- // Ensure parent directory exists
59+ // Ensure parent directory exists with restricted permissions
6360 if let Some ( parent) = std:: path:: Path :: new ( & self . socket_path ) . parent ( ) {
6461 std:: fs:: create_dir_all ( parent)
6562 . map_err ( |e| format ! ( "Failed to create socket directory: {e}" ) ) ?;
63+ // Restrict directory to owner only (0700) — prevents other users
64+ // from listing socket files or key filenames
65+ use std:: os:: unix:: fs:: PermissionsExt ;
66+ let _ = std:: fs:: set_permissions (
67+ parent,
68+ std:: fs:: Permissions :: from_mode ( 0o700 ) ,
69+ ) ;
6670 }
6771
72+ // Acquire exclusive lock before touching the socket file.
73+ // Prevents a TOCTOU race where a malicious process could create a fake
74+ // socket between our unlink() and bind(), intercepting client connections.
75+ let lock_path = format ! ( "{}.lock" , self . socket_path) ;
76+ let lock_fd = {
77+ use std:: os:: unix:: fs:: OpenOptionsExt ;
78+ std:: fs:: OpenOptions :: new ( )
79+ . read ( true )
80+ . write ( true )
81+ . create ( true )
82+ . mode ( 0o600 )
83+ . open ( & lock_path)
84+ . map_err ( |e| format ! ( "Failed to create lock file: {e}" ) ) ?
85+ } ;
86+ use std:: os:: unix:: io:: AsRawFd ;
87+ let lock_result = unsafe { libc:: flock ( lock_fd. as_raw_fd ( ) , libc:: LOCK_EX | libc:: LOCK_NB ) } ;
88+ if lock_result != 0 {
89+ return Err ( "Another daemon instance holds the lock" . into ( ) ) ;
90+ }
91+
92+ // Safe to remove stale socket now — we hold the lock
93+ let _ = std:: fs:: remove_file ( & self . socket_path ) ;
94+
6895 let listener = UnixListener :: bind ( & self . socket_path )
6996 . map_err ( |e| format ! ( "Socket bind failed: {e}" ) ) ?;
7097
@@ -96,6 +123,12 @@ impl IpcServer {
96123 let on_activity = self . on_activity . clone ( ) ;
97124 let running = self . running . clone ( ) ;
98125
126+ // Verify the connecting process is a trusted varlock binary
127+ if !verify_unix_client ( & stream) {
128+ eprintln ! ( "Rejected connection from untrusted process" ) ;
129+ continue ;
130+ }
131+
99132 // Get peer TTY identity
100133 let tty_id = get_peer_tty_id ( & stream) ;
101134
@@ -116,8 +149,9 @@ impl IpcServer {
116149 }
117150 }
118151
119- // Cleanup
152+ // Cleanup socket and lock
120153 let _ = std:: fs:: remove_file ( & self . socket_path ) ;
154+ let _ = std:: fs:: remove_file ( format ! ( "{}.lock" , self . socket_path) ) ;
121155 Ok ( ( ) )
122156 }
123157
@@ -227,6 +261,7 @@ impl Drop for IpcServer {
227261 fn drop ( & mut self ) {
228262 self . stop ( ) ;
229263 let _ = std:: fs:: remove_file ( & self . socket_path ) ;
264+ let _ = std:: fs:: remove_file ( format ! ( "{}.lock" , self . socket_path) ) ;
230265 }
231266}
232267
@@ -309,6 +344,60 @@ fn send_response(stream: &mut impl Write, id: Option<&str>, response: &Value) ->
309344 Ok ( ( ) )
310345}
311346
347+ // ── Unix client process verification ─────────────────────────────
348+
349+ /// Verify that the connecting client process is a trusted varlock binary.
350+ /// Uses peer credentials to get the client PID, then reads /proc/<pid>/exe.
351+ #[ cfg( target_os = "linux" ) ]
352+ fn verify_unix_client ( stream : & UnixStream ) -> bool {
353+ use nix:: sys:: socket:: { getsockopt, sockopt:: PeerCredentials } ;
354+ use std:: os:: fd:: AsFd ;
355+
356+ let creds = match getsockopt ( & stream. as_fd ( ) , PeerCredentials ) {
357+ Ok ( c) => c,
358+ Err ( _) => return false ,
359+ } ;
360+
361+ let pid = creds. pid ( ) ;
362+ if pid <= 0 {
363+ return false ;
364+ }
365+
366+ // Read the executable path via /proc/<pid>/exe symlink
367+ let exe_path = match std:: fs:: read_link ( format ! ( "/proc/{pid}/exe" ) ) {
368+ Ok ( p) => p,
369+ Err ( _) => return false ,
370+ } ;
371+
372+ let exe_name = exe_path
373+ . file_name ( )
374+ . and_then ( |n| n. to_str ( ) )
375+ . unwrap_or ( "" ) ;
376+
377+ let allowed = [
378+ "varlock-local-encrypt" ,
379+ "varlock" ,
380+ "node" ,
381+ "bun" ,
382+ ] ;
383+
384+ let is_trusted = allowed. iter ( ) . any ( |name| exe_name == * name) ;
385+ if !is_trusted {
386+ eprintln ! (
387+ "Untrusted client process: PID={pid}, exe={}" ,
388+ exe_path. display( )
389+ ) ;
390+ }
391+ is_trusted
392+ }
393+
394+ /// macOS: no /proc filesystem, skip process verification for now.
395+ /// The Unix socket 0600 permissions still restrict to the owning user.
396+ #[ cfg( all( unix, not( target_os = "linux" ) ) ) ]
397+ fn verify_unix_client ( _stream : & UnixStream ) -> bool {
398+ true
399+ }
400+
312401// ── Peer TTY identity (Linux) ────────────────────────────────────
313402
314403#[ cfg( target_os = "linux" ) ]
0 commit comments