Skip to main content

gate4agent_node_wire/
unix_secure_socket.rs

1use std::ffi::OsString;
2use std::fs::{self, File, Metadata, OpenOptions, Permissions};
3use std::io;
4use std::mem::MaybeUninit;
5use std::os::fd::AsRawFd;
6use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt, PermissionsExt};
7use std::path::{Path, PathBuf};
8use std::time::Duration;
9
10use tokio::net::{UnixListener, UnixStream};
11use tokio::time::{sleep, timeout};
12
13const CONNECT_RETRIES: usize = 100;
14const CONNECT_RETRY_DELAY: Duration = Duration::from_millis(20);
15const STALE_SOCKET_PROBE_TIMEOUT: Duration = Duration::from_millis(250);
16
17pub type LocalServerStream = UnixStream;
18pub type LocalClientStream = UnixStream;
19
20#[derive(Debug)]
21pub struct OwnerOnlyLocalListener {
22    listener: UnixListener,
23    endpoint: PathBuf,
24    identity: FileIdentity,
25    lock: EndpointLock,
26}
27
28impl OwnerOnlyLocalListener {
29    pub async fn bind(endpoint: impl AsRef<Path>) -> io::Result<Self> {
30        let owner_uid = current_euid();
31        let endpoint = validated_endpoint(endpoint.as_ref(), owner_uid)?;
32        let lock = EndpointLock::acquire(&endpoint, owner_uid)?;
33        remove_stale_socket(&endpoint, owner_uid, &lock).await?;
34
35        let listener = UnixListener::bind(&endpoint)?;
36        validate_listener(&listener)?;
37        let initial_metadata = match owned_socket_metadata(&endpoint, owner_uid) {
38            Ok(metadata) => metadata,
39            Err(error) => {
40                drop(listener);
41                return Err(error);
42            }
43        };
44        let initial_identity = FileIdentity::from_metadata(&initial_metadata);
45
46        if let Err(error) = fs::set_permissions(&endpoint, Permissions::from_mode(0o600)) {
47            drop(listener);
48            remove_socket_if_same(&endpoint, initial_identity, &lock);
49            return Err(error);
50        }
51
52        let metadata = match owned_socket_metadata(&endpoint, owner_uid) {
53            Ok(metadata)
54                if FileIdentity::from_metadata(&metadata) == initial_identity
55                    && metadata.permissions().mode() & 0o7777 == 0o600 =>
56            {
57                metadata
58            }
59            Ok(_) => {
60                drop(listener);
61                remove_socket_if_same(&endpoint, initial_identity, &lock);
62                return Err(io::Error::new(
63                    io::ErrorKind::PermissionDenied,
64                    format!(
65                        "local socket changed while securing {}",
66                        endpoint.display()
67                    ),
68                ));
69            }
70            Err(error) => {
71                drop(listener);
72                remove_socket_if_same(&endpoint, initial_identity, &lock);
73                return Err(error);
74            }
75        };
76
77        Ok(Self {
78            listener,
79            endpoint,
80            identity: FileIdentity::from_metadata(&metadata),
81            lock,
82        })
83    }
84
85    pub async fn accept(&mut self) -> io::Result<LocalServerStream> {
86        loop {
87            let (stream, _) = self.listener.accept().await?;
88            let peer_uid = stream.peer_cred()?.uid();
89            if trusted_peer_uid(peer_uid, self.identity.uid) {
90                return Ok(stream);
91            }
92            drop(stream);
93        }
94    }
95}
96
97impl Drop for OwnerOnlyLocalListener {
98    fn drop(&mut self) {
99        remove_socket_if_same(&self.endpoint, self.identity, &self.lock);
100    }
101}
102
103pub async fn connect_local_stream(endpoint: impl AsRef<Path>) -> io::Result<LocalClientStream> {
104    let owner_uid = current_euid();
105    let endpoint = validated_endpoint(endpoint.as_ref(), owner_uid)?;
106    let mut last_error = None;
107
108    for attempt in 0..CONNECT_RETRIES {
109        match UnixStream::connect(&endpoint).await {
110            Ok(stream) => {
111                let peer_uid = stream.peer_cred()?.uid();
112                if trusted_peer_uid(peer_uid, owner_uid) {
113                    return Ok(stream);
114                }
115                return Err(io::Error::new(
116                    io::ErrorKind::PermissionDenied,
117                    format!(
118                        "local socket peer for {} has untrusted uid {peer_uid}",
119                        endpoint.display()
120                    ),
121                ));
122            }
123            Err(error) if retryable_connect_error(&error) => {
124                let has_more_attempts = attempt + 1 < CONNECT_RETRIES;
125                last_error = Some(error);
126                if has_more_attempts {
127                    sleep(CONNECT_RETRY_DELAY).await;
128                }
129            }
130            Err(error) => return Err(error),
131        }
132    }
133
134    Err(last_error.unwrap_or_else(|| {
135        io::Error::new(
136            io::ErrorKind::NotFound,
137            format!("local socket was not available at {}", endpoint.display()),
138        )
139    }))
140}
141
142#[derive(Clone, Copy, Debug, Eq, PartialEq)]
143struct FileIdentity {
144    device: u64,
145    inode: u64,
146    uid: u32,
147}
148
149impl FileIdentity {
150    fn from_metadata(metadata: &Metadata) -> Self {
151        Self {
152            device: metadata.dev(),
153            inode: metadata.ino(),
154            uid: metadata.uid(),
155        }
156    }
157}
158
159#[derive(Debug)]
160struct EndpointLock {
161    _file: File,
162}
163
164impl EndpointLock {
165    fn acquire(endpoint: &Path, owner_uid: u32) -> io::Result<Self> {
166        let path = endpoint_lock_path(endpoint);
167        let file = OpenOptions::new()
168            .read(true)
169            .write(true)
170            .create(true)
171            .mode(0o600)
172            .custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK)
173            .open(&path)?;
174        let metadata = file.metadata()?;
175        if !metadata.file_type().is_file() {
176            return Err(io::Error::new(
177                io::ErrorKind::InvalidInput,
178                format!("local socket lock is not a regular file: {}", path.display()),
179            ));
180        }
181        if metadata.uid() != owner_uid {
182            return Err(io::Error::new(
183                io::ErrorKind::PermissionDenied,
184                format!(
185                    "local socket lock {} is owned by uid {}, expected {owner_uid}",
186                    path.display(),
187                    metadata.uid()
188                ),
189            ));
190        }
191        let identity = FileIdentity::from_metadata(&metadata);
192        let path_metadata = fs::symlink_metadata(&path)?;
193        if !path_metadata.file_type().is_file()
194            || FileIdentity::from_metadata(&path_metadata) != identity
195        {
196            return Err(io::Error::new(
197                io::ErrorKind::PermissionDenied,
198                format!("local socket lock path changed: {}", path.display()),
199            ));
200        }
201
202        let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
203        if result != 0 {
204            let error = io::Error::last_os_error();
205            let raw_error = error.raw_os_error();
206            if raw_error == Some(libc::EWOULDBLOCK) || raw_error == Some(libc::EAGAIN) {
207                return Err(io::Error::new(
208                    io::ErrorKind::AddrInUse,
209                    format!("local socket lock is already held: {}", path.display()),
210                ));
211            }
212            return Err(error);
213        }
214
215        file.set_permissions(Permissions::from_mode(0o600))?;
216        let secured_metadata = file.metadata()?;
217        let secured_path_metadata = fs::symlink_metadata(&path)?;
218        let secured_identity = FileIdentity::from_metadata(&secured_metadata);
219        if !secured_metadata.file_type().is_file()
220            || secured_metadata.uid() != owner_uid
221            || secured_metadata.permissions().mode() & 0o7777 != 0o600
222            || secured_identity != identity
223            || !secured_path_metadata.file_type().is_file()
224            || FileIdentity::from_metadata(&secured_path_metadata) != identity
225        {
226            return Err(io::Error::new(
227                io::ErrorKind::PermissionDenied,
228                format!("local socket lock is not securely fixed: {}", path.display()),
229            ));
230        }
231
232        Ok(Self { _file: file })
233    }
234}
235
236fn current_euid() -> u32 {
237    unsafe { libc::geteuid() }
238}
239
240// UID 0 is the Unix analogue of the LocalSystem identity trusted by the
241// Windows transport. Peer credentials are checked before any application
242// bytes are read or written.
243fn trusted_peer_uid(peer_uid: u32, owner_uid: u32) -> bool {
244    peer_uid == owner_uid || peer_uid == 0
245}
246
247fn validated_endpoint(endpoint: &Path, owner_uid: u32) -> io::Result<PathBuf> {
248    if !endpoint.is_absolute() {
249        return Err(io::Error::new(
250            io::ErrorKind::InvalidInput,
251            format!("local socket endpoint must be absolute: {}", endpoint.display()),
252        ));
253    }
254    let file_name = endpoint.file_name().ok_or_else(|| {
255        io::Error::new(
256            io::ErrorKind::InvalidInput,
257            format!(
258                "local socket endpoint must end in a filename: {}",
259                endpoint.display()
260            ),
261        )
262    })?;
263    let parent = endpoint.parent().ok_or_else(|| {
264        io::Error::new(
265            io::ErrorKind::InvalidInput,
266            format!(
267                "local socket endpoint has no parent directory: {}",
268                endpoint.display()
269            ),
270        )
271    })?;
272    let parent = fs::canonicalize(parent)?;
273    let metadata = fs::symlink_metadata(&parent)?;
274    if !metadata.file_type().is_dir() {
275        return Err(io::Error::new(
276            io::ErrorKind::InvalidInput,
277            format!("local socket parent is not a directory: {}", parent.display()),
278        ));
279    }
280    if metadata.uid() != owner_uid {
281        return Err(io::Error::new(
282            io::ErrorKind::PermissionDenied,
283            format!(
284                "local socket parent {} is owned by uid {}, expected {owner_uid}",
285                parent.display(),
286                metadata.uid()
287            ),
288        ));
289    }
290    let mode = metadata.permissions().mode() & 0o7777;
291    if mode != 0o700 {
292        return Err(io::Error::new(
293            io::ErrorKind::PermissionDenied,
294            format!(
295                "local socket parent {} has mode {mode:04o}, expected 0700",
296                parent.display()
297            ),
298        ));
299    }
300
301    Ok(parent.join(file_name))
302}
303
304fn endpoint_lock_path(endpoint: &Path) -> PathBuf {
305    let mut lock_name = OsString::from(".");
306    lock_name.push(
307        endpoint
308            .file_name()
309            .expect("validated endpoint has a filename"),
310    );
311    lock_name.push(".gate4agent.lock");
312    endpoint.with_file_name(lock_name)
313}
314
315fn validate_listener(listener: &UnixListener) -> io::Result<()> {
316    let mut stat = MaybeUninit::<libc::stat>::uninit();
317    if unsafe { libc::fstat(listener.as_raw_fd(), stat.as_mut_ptr()) } != 0 {
318        return Err(io::Error::last_os_error());
319    }
320    let stat = unsafe { stat.assume_init() };
321    if stat.st_mode & libc::S_IFMT != libc::S_IFSOCK {
322        return Err(io::Error::new(
323            io::ErrorKind::PermissionDenied,
324            "bound listener file descriptor is not a Unix socket",
325        ));
326    }
327    Ok(())
328}
329
330async fn remove_stale_socket(
331    endpoint: &Path,
332    owner_uid: u32,
333    _lock: &EndpointLock,
334) -> io::Result<()> {
335    let metadata = match fs::symlink_metadata(endpoint) {
336        Ok(metadata) => metadata,
337        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
338        Err(error) => return Err(error),
339    };
340    if !metadata.file_type().is_socket() {
341        return Err(io::Error::new(
342            io::ErrorKind::AlreadyExists,
343            format!(
344                "refusing to replace non-socket endpoint {}",
345                endpoint.display()
346            ),
347        ));
348    }
349    if metadata.uid() != owner_uid {
350        return Err(io::Error::new(
351            io::ErrorKind::PermissionDenied,
352            format!(
353                "refusing to replace socket {} owned by uid {}",
354                endpoint.display(),
355                metadata.uid()
356            ),
357        ));
358    }
359    let original_identity = FileIdentity::from_metadata(&metadata);
360
361    match timeout(STALE_SOCKET_PROBE_TIMEOUT, UnixStream::connect(endpoint)).await {
362        Ok(Ok(stream)) => {
363            drop(stream);
364            return Err(live_socket_collision(endpoint));
365        }
366        Err(_) => return Err(live_socket_collision(endpoint)),
367        Ok(Err(error)) if error.kind() == io::ErrorKind::ConnectionRefused => {}
368        Ok(Err(error)) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
369        Ok(Err(error)) => return Err(error),
370    }
371
372    let current_metadata = match fs::symlink_metadata(endpoint) {
373        Ok(metadata) => metadata,
374        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
375        Err(error) => return Err(error),
376    };
377    let unchanged_owned_socket = current_metadata.file_type().is_socket()
378        && current_metadata.uid() == owner_uid
379        && FileIdentity::from_metadata(&current_metadata) == original_identity;
380    if !unchanged_owned_socket {
381        return Err(io::Error::new(
382            io::ErrorKind::AlreadyExists,
383            format!(
384                "local socket endpoint changed during stale probe: {}",
385                endpoint.display()
386            ),
387        ));
388    }
389
390    fs::remove_file(endpoint)
391}
392
393fn owned_socket_metadata(endpoint: &Path, owner_uid: u32) -> io::Result<Metadata> {
394    let metadata = fs::symlink_metadata(endpoint)?;
395    if !metadata.file_type().is_socket() || metadata.uid() != owner_uid {
396        return Err(io::Error::new(
397            io::ErrorKind::PermissionDenied,
398            format!(
399                "bound endpoint is not an owned socket: {}",
400                endpoint.display()
401            ),
402        ));
403    }
404    Ok(metadata)
405}
406
407fn remove_socket_if_same(endpoint: &Path, identity: FileIdentity, _lock: &EndpointLock) {
408    let Ok(metadata) = fs::symlink_metadata(endpoint) else {
409        return;
410    };
411    if metadata.file_type().is_socket()
412        && FileIdentity::from_metadata(&metadata) == identity
413    {
414        let _ = fs::remove_file(endpoint);
415    }
416}
417
418fn retryable_connect_error(error: &io::Error) -> bool {
419    matches!(
420        error.kind(),
421        io::ErrorKind::NotFound | io::ErrorKind::ConnectionRefused
422    )
423}
424
425fn live_socket_collision(endpoint: &Path) -> io::Error {
426    io::Error::new(
427        io::ErrorKind::AddrInUse,
428        format!("live local socket already exists at {}", endpoint.display()),
429    )
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use std::fs::File;
436    use std::os::unix::fs::symlink;
437    use std::sync::atomic::{AtomicU64, Ordering};
438    use tokio::io::{AsyncReadExt, AsyncWriteExt};
439
440    static TEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
441
442    struct TestDirectory {
443        path: PathBuf,
444    }
445
446    impl TestDirectory {
447        fn new() -> Self {
448            let base = fs::canonicalize(std::env::temp_dir()).expect("canonical temp directory");
449            for _ in 0..100 {
450                let sequence = TEST_SEQUENCE.fetch_add(1, Ordering::Relaxed);
451                let path = base.join(format!("g{:x}-{sequence:x}", std::process::id()));
452                match fs::create_dir(&path) {
453                    Ok(()) => {
454                        fs::set_permissions(&path, Permissions::from_mode(0o700))
455                            .expect("secure test directory");
456                        return Self { path };
457                    }
458                    Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
459                    Err(error) => panic!("create test directory: {error}"),
460                }
461            }
462            panic!("allocate unique short test directory")
463        }
464
465        fn endpoint(&self, name: &str) -> PathBuf {
466            self.path.join(name)
467        }
468    }
469
470    impl Drop for TestDirectory {
471        fn drop(&mut self) {
472            if let Ok(entries) = fs::read_dir(&self.path) {
473                for entry in entries.flatten() {
474                    match entry.file_type() {
475                        Ok(file_type) if file_type.is_dir() => {
476                            let _ = fs::remove_dir(entry.path());
477                        }
478                        _ => {
479                            let _ = fs::remove_file(entry.path());
480                        }
481                    }
482                }
483            }
484            let _ = fs::remove_dir(&self.path);
485        }
486    }
487
488    #[tokio::test(flavor = "current_thread")]
489    async fn owner_only_socket_has_secure_modes_and_roundtrips() {
490        let directory = TestDirectory::new();
491        let endpoint = directory.endpoint("n.sock");
492        let lock_path = endpoint_lock_path(&endpoint);
493        assert_eq!(
494            lock_path.file_name(),
495            Some(std::ffi::OsStr::new(".n.sock.gate4agent.lock"))
496        );
497        let mut listener = OwnerOnlyLocalListener::bind(&endpoint)
498            .await
499            .expect("bind owner-only socket");
500
501        assert_eq!(
502            fs::metadata(&directory.path)
503                .expect("stat parent")
504                .permissions()
505                .mode()
506                & 0o7777,
507            0o700
508        );
509        assert_eq!(
510            fs::symlink_metadata(&endpoint)
511                .expect("stat socket")
512                .permissions()
513                .mode()
514                & 0o7777,
515            0o600
516        );
517        let lock_metadata = fs::symlink_metadata(&lock_path).expect("stat endpoint lock");
518        assert!(lock_metadata.file_type().is_file());
519        assert_eq!(lock_metadata.uid(), current_euid());
520        assert_eq!(lock_metadata.permissions().mode() & 0o7777, 0o600);
521
522        let server = tokio::spawn(async move {
523            let mut stream = listener.accept().await.expect("accept owner client");
524            let mut request = [0_u8; 4];
525            stream.read_exact(&mut request).await.expect("read request");
526            assert_eq!(&request, b"ping");
527            stream.write_all(b"pong").await.expect("write response");
528        });
529        let mut client = connect_local_stream(&endpoint)
530            .await
531            .expect("connect owner client");
532        client.write_all(b"ping").await.expect("write request");
533        let mut response = [0_u8; 4];
534        client.read_exact(&mut response).await.expect("read response");
535        assert_eq!(&response, b"pong");
536        server.await.expect("join server");
537        assert!(lock_path.is_file(), "listener drop must retain lock file");
538    }
539
540    #[tokio::test(flavor = "current_thread")]
541    async fn cancelled_accept_keeps_listener_usable() {
542        let directory = TestDirectory::new();
543        let endpoint = directory.endpoint("n.sock");
544        let mut listener = OwnerOnlyLocalListener::bind(&endpoint)
545            .await
546            .expect("bind owner-only socket");
547
548        timeout(Duration::from_millis(10), listener.accept())
549            .await
550            .expect_err("accept remains pending without a client");
551        let client = connect_local_stream(&endpoint)
552            .await
553            .expect("connect after cancelled accept");
554        let server = listener.accept().await.expect("accept after cancellation");
555        drop(client);
556        drop(server);
557    }
558
559    #[tokio::test(flavor = "current_thread")]
560    async fn second_listener_lock_collision_does_not_remove_live_socket() {
561        let directory = TestDirectory::new();
562        let endpoint = directory.endpoint("n.sock");
563        let listener = OwnerOnlyLocalListener::bind(&endpoint)
564            .await
565            .expect("bind first listener");
566        let identity = FileIdentity::from_metadata(
567            &fs::symlink_metadata(&endpoint).expect("stat first socket"),
568        );
569        let lock_path = endpoint_lock_path(&endpoint);
570        let lock_identity = FileIdentity::from_metadata(
571            &fs::symlink_metadata(&lock_path).expect("stat held endpoint lock"),
572        );
573
574        let error = OwnerOnlyLocalListener::bind(&endpoint)
575            .await
576            .expect_err("reject live collision");
577        assert_eq!(error.kind(), io::ErrorKind::AddrInUse);
578        assert_eq!(
579            FileIdentity::from_metadata(
580                &fs::symlink_metadata(&endpoint).expect("live socket remains")
581            ),
582            identity
583        );
584        assert_eq!(
585            FileIdentity::from_metadata(
586                &fs::symlink_metadata(&lock_path).expect("held endpoint lock remains")
587            ),
588            lock_identity
589        );
590        drop(listener);
591    }
592
593    #[tokio::test(flavor = "current_thread")]
594    async fn endpoint_specific_locks_allow_two_listeners_in_one_parent() {
595        let directory = TestDirectory::new();
596        let node_endpoint = directory.endpoint("n.sock");
597        let c2_endpoint = directory.endpoint("c.sock");
598        assert_ne!(
599            endpoint_lock_path(&node_endpoint),
600            endpoint_lock_path(&c2_endpoint)
601        );
602
603        let node_listener = OwnerOnlyLocalListener::bind(&node_endpoint)
604            .await
605            .expect("bind node listener");
606        let c2_listener = OwnerOnlyLocalListener::bind(&c2_endpoint)
607            .await
608            .expect("bind C2 listener in same parent");
609        assert!(node_endpoint.exists());
610        assert!(c2_endpoint.exists());
611        drop(c2_listener);
612        drop(node_listener);
613    }
614
615    #[tokio::test(flavor = "current_thread")]
616    async fn safely_recovers_owned_stale_socket() {
617        let directory = TestDirectory::new();
618        let endpoint = directory.endpoint("n.sock");
619        let stale = std::os::unix::net::UnixListener::bind(&endpoint)
620            .expect("bind stale socket fixture");
621        drop(stale);
622        let mut listener = OwnerOnlyLocalListener::bind(&endpoint)
623            .await
624            .expect("recover stale socket");
625        let client = connect_local_stream(&endpoint)
626            .await
627            .expect("connect to recovered socket");
628        let server = listener
629            .accept()
630            .await
631            .expect("accept on recovered socket");
632        drop(client);
633        drop(server);
634        drop(listener);
635    }
636
637    #[tokio::test(flavor = "current_thread")]
638    async fn refuses_regular_file_and_symlink_endpoints() {
639        let directory = TestDirectory::new();
640        let regular = directory.endpoint("regular");
641        File::create(&regular).expect("create regular endpoint fixture");
642        let regular_error = OwnerOnlyLocalListener::bind(&regular)
643            .await
644            .expect_err("refuse regular file");
645        assert_eq!(regular_error.kind(), io::ErrorKind::AlreadyExists);
646        assert!(regular.is_file());
647
648        let target = directory.endpoint("target");
649        File::create(&target).expect("create symlink target");
650        let link = directory.endpoint("link.sock");
651        symlink(&target, &link).expect("create endpoint symlink");
652        let symlink_error = OwnerOnlyLocalListener::bind(&link)
653            .await
654            .expect_err("refuse endpoint symlink");
655        assert_eq!(symlink_error.kind(), io::ErrorKind::AlreadyExists);
656        assert!(
657            fs::symlink_metadata(&link)
658                .expect("symlink remains")
659                .file_type()
660                .is_symlink()
661        );
662        assert!(target.is_file());
663    }
664
665    #[tokio::test(flavor = "current_thread")]
666    async fn rejects_relative_endpoint_and_insecure_parent_mode() {
667        let relative_error = OwnerOnlyLocalListener::bind(Path::new("n.sock"))
668            .await
669            .expect_err("reject relative endpoint");
670        assert_eq!(relative_error.kind(), io::ErrorKind::InvalidInput);
671
672        let directory = TestDirectory::new();
673        fs::set_permissions(&directory.path, Permissions::from_mode(0o750))
674            .expect("make parent insecure");
675        let endpoint = directory.endpoint("n.sock");
676        let mode_error = OwnerOnlyLocalListener::bind(&endpoint)
677            .await
678            .expect_err("reject insecure parent mode");
679        assert_eq!(mode_error.kind(), io::ErrorKind::PermissionDenied);
680        assert!(!endpoint.exists());
681        fs::set_permissions(&directory.path, Permissions::from_mode(0o700))
682            .expect("restore parent mode for cleanup");
683    }
684
685    #[tokio::test(flavor = "current_thread")]
686    async fn refuses_symlink_and_nonregular_endpoint_locks() {
687        let directory = TestDirectory::new();
688
689        let symlink_endpoint = directory.endpoint("s.sock");
690        let symlink_lock = endpoint_lock_path(&symlink_endpoint);
691        let target = directory.endpoint("lock-target");
692        File::create(&target).expect("create lock symlink target");
693        symlink(&target, &symlink_lock).expect("create malicious lock symlink");
694        OwnerOnlyLocalListener::bind(&symlink_endpoint)
695            .await
696            .expect_err("refuse symlink lock");
697        assert!(
698            fs::symlink_metadata(&symlink_lock)
699                .expect("malicious lock symlink remains")
700                .file_type()
701                .is_symlink()
702        );
703        assert!(!symlink_endpoint.exists());
704
705        let directory_endpoint = directory.endpoint("d.sock");
706        let directory_lock = endpoint_lock_path(&directory_endpoint);
707        fs::create_dir(&directory_lock).expect("create nonregular lock fixture");
708        OwnerOnlyLocalListener::bind(&directory_endpoint)
709            .await
710            .expect_err("refuse nonregular lock");
711        assert!(directory_lock.is_dir());
712        assert!(!directory_endpoint.exists());
713    }
714
715    #[tokio::test(flavor = "current_thread")]
716    async fn drop_preserves_replacement_socket_inode() {
717        let directory = TestDirectory::new();
718        let endpoint = directory.endpoint("n.sock");
719        let lock_path = endpoint_lock_path(&endpoint);
720        let listener = OwnerOnlyLocalListener::bind(&endpoint)
721            .await
722            .expect("bind managed socket");
723        fs::remove_file(&endpoint).expect("unlink managed socket fixture");
724        let replacement = std::os::unix::net::UnixListener::bind(&endpoint)
725            .expect("bind replacement socket");
726        let replacement_identity = FileIdentity::from_metadata(
727            &fs::symlink_metadata(&endpoint).expect("stat replacement socket"),
728        );
729
730        let collision = OwnerOnlyLocalListener::bind(&endpoint)
731            .await
732            .expect_err("held endpoint lock blocks bind after path replacement");
733        assert_eq!(collision.kind(), io::ErrorKind::AddrInUse);
734        assert_eq!(
735            FileIdentity::from_metadata(
736                &fs::symlink_metadata(&endpoint).expect("replacement survives collision")
737            ),
738            replacement_identity
739        );
740
741        drop(listener);
742        assert_eq!(
743            FileIdentity::from_metadata(
744                &fs::symlink_metadata(&endpoint).expect("replacement socket remains")
745            ),
746            replacement_identity
747        );
748        assert!(lock_path.is_file(), "replacement race must retain lock file");
749        drop(replacement);
750    }
751
752    #[tokio::test(flavor = "current_thread")]
753    async fn drop_cleans_socket_and_allows_rebind() {
754        let directory = TestDirectory::new();
755        let endpoint = directory.endpoint("n.sock");
756        let lock_path = endpoint_lock_path(&endpoint);
757        let listener = OwnerOnlyLocalListener::bind(&endpoint)
758            .await
759            .expect("bind first socket");
760        assert!(endpoint.exists());
761        drop(listener);
762        assert!(!endpoint.exists());
763        let lock_identity = FileIdentity::from_metadata(
764            &fs::symlink_metadata(&lock_path).expect("lock remains after cleanup"),
765        );
766
767        let rebound = OwnerOnlyLocalListener::bind(&endpoint)
768            .await
769            .expect("rebind after cleanup");
770        assert!(endpoint.exists());
771        drop(rebound);
772        assert!(!endpoint.exists());
773        assert_eq!(
774            FileIdentity::from_metadata(
775                &fs::symlink_metadata(&lock_path).expect("same lock remains after rebind")
776            ),
777            lock_identity
778        );
779    }
780}