trusty-common 0.52.4

Shared utilities and provider-agnostic streaming chat (ChatProvider, OllamaProvider, OpenRouter, tool-use) for trusty-* projects
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
//! Permission enforcement for Unix-domain sockets — the `0600` guarantee
//! ADR-0031 and ADR-0032 cite as an existing property.
//!
//! Why: both ADRs rest their case for UDS-over-loopback-TCP on "a loopback port
//! is reachable by any local process, a `0600` socket is not". Until #5099 no
//! production code in this workspace called `set_permissions` on any socket —
//! every hit was a test fixture. Sockets were created at the process umask
//! (commonly `0755`) and two of the three path conventions placed them in
//! `$TMPDIR` falling back to a shared, world-writable `/tmp`. This module makes
//! the claimed guarantee real, in one place, so a behavior fix lands once
//! rather than at four bind sites. ADR-0034 §3 ("The trust boundary") states
//! the requirement: `0700` directory, `0600` socket, peer-uid check on accept.
//!
//! What: four primitives, in the order a daemon uses them.
//!   - [`scratch_socket_dir`] resolves a per-uid directory under the system
//!     scratch space, replacing the bare `$TMPDIR`-with-`/tmp`-fallback.
//!   - [`bind_hardened`] creates that directory at `0700`, binds, and sets the
//!     socket to `0600` before returning it to the caller.
//!   - [`connect_hardened`] is the dialer's half: it verifies the directory and
//!     socket a client is about to trust before writing anything to it.
//!   - [`ensure_peer_is_self`] refuses an accepted connection whose peer uid is
//!     not this process's own, which is what turns the permission bits into an
//!     enforced boundary rather than a documented intention.
//!
//! [`sockbuf`] runs inside [`bind_hardened`], [`connect_hardened`] and
//! [`accept_sized`], so both ends of every socket here are sized to
//! [`sockbuf::SOCKET_BUFFER_BYTES`] rather than the 8 KiB macOS default
//! (#6896). Only macOS copies a listener's sizing onto the sockets `accept`
//! returns, so a service driving its own accept loop needs [`accept_sized`] in
//! place of `listener.accept()` to be covered on Linux.
//!
//! **What this does and does not guarantee.** With the directory held at `0700`
//! and owned by this uid, no other unprivileged user can traverse to the socket
//! — that is the load-bearing property, and it holds from the moment the
//! directory is created. It is *not* an unconditional claim about every path
//! this module can be handed: a caller that points it at a directory an
//! attacker can rename or unlink entries in retains a residual swap race that
//! only `openat`/`fchmod` on a directory fd could close. Symlink pre-creation,
//! which is the practical version of that attack, is refused outright, as is a
//! non-directory sitting at the socket-directory path — see
//! [`dir::prepare_socket_dir`]. Root is not defended against and cannot be.
//!
//! Deliberately not a transport: there is no framing or JSON-RPC here. #5089
//! step 1 builds the shared UDS transport module; this is the security layer
//! that module mounts, including the dialer it needs, not a competing
//! implementation of it.
//!
//! Test: `tests.rs` — directory and socket modes after a real bind, the
//! pre-existing-wide-directory repair, symlink refusal, the pure decision
//! functions behind every refusal, and the `sun_path` budget pre-check.
//!
//! [`scratch_socket_dir`]: crate::uds::scratch_socket_dir
//! [`accept_sized`]: crate::uds::accept_sized
//! [`bind_hardened`]: crate::uds::bind_hardened
//! [`connect_hardened`]: crate::uds::connect_hardened
//! [`ensure_peer_is_self`]: crate::uds::ensure_peer_is_self
//! [`dir::prepare_socket_dir`]: crate::uds::dir::prepare_socket_dir
//! [`sockbuf`]: crate::uds::sockbuf
//! [`sockbuf::SOCKET_BUFFER_BYTES`]: crate::uds::sockbuf::SOCKET_BUFFER_BYTES

#[cfg(test)]
#[path = "tests.rs"]
mod tests;

pub mod dir;
mod peer;
pub mod probe;
// #8267: the bounded connect retry every framed dial runs under, so "the
// daemon refused one connect" stops being a session-long MCP failure.
pub mod retry;
pub mod rpc;
// #6277: the serving half of `rpc`, so a daemon migrating off HTTP under
// ADR-0032 supplies a method table rather than a fourth hand-rolled accept loop.
pub mod server;
pub mod singleton;
// #6896: one place that sizes SO_SNDBUF/SO_RCVBUF, so both ends of every socket
// this module owns are sized identically and no consumer sets them itself.
pub mod sockbuf;
// #6637: the SSE rendering of an opened stream, hoisted out of trusty-console
// so a second UI crate bridging a webview onto a socket shares it rather than
// carrying a third copy. Behind `axum-server` because it is axum types all the
// way down; a crate that only dials a socket must not pull axum in.
#[cfg(feature = "axum-server")]
pub mod sse;
// #6286: the reading half of a multi-frame response, so a token stream has one
// definition of what terminates it rather than one per consumer.
pub mod stream_client;

/// On-demand supervision of a UDS-serving child process (#5089 step 2).
///
/// Why: ADR-0034 §1 needs `trusty-console` to start `trusty-review` /
/// `trusty-analyze` at delivery time so neither has to be resident. The
/// mechanism ADR-0032 said "does not yet exist" did exist, as `trusty-memory`'s
/// `Bm25Supervisor`; this is that supervisor with its BM25-specific parts lifted
/// into per-service configuration.
/// What: [`supervisor::UdsServiceSupervisor`] plus its config, error and probe
/// types. Gated behind the `uds-supervisor` feature because it pulls the whole
/// child-process lifecycle in, which a crate that only binds a socket does not
/// need.
/// Test: `supervisor/tests.rs`, and `trusty-memory`'s
/// `tests/bm25_supervisor_concurrency.rs` against real children.
#[cfg(feature = "uds-supervisor")]
pub mod supervisor;

/// The single entry point for starting `trusty-analyze` on demand (#6350).
///
/// Why it sits beside [`supervisor`] rather than inside it: the supervisor is
/// the generic machinery, and this is the one service description every client
/// crate shares. Gated behind the same feature, because it is that machinery
/// applied.
/// Test: `on_demand_tests.rs`.
#[cfg(feature = "uds-supervisor")]
pub mod on_demand;

pub use dir::prepare_socket_dir;
#[cfg(feature = "uds-supervisor")]
pub use on_demand::{
    ANALYZE_EXTERNAL_ENV, ANALYZE_IDLE_TIMEOUT_ENV, ANALYZE_SERVICE, ANALYZE_SHUTDOWN_FLUSH,
    DEFAULT_ANALYZE_IDLE_TIMEOUT, OnDemandAnalyze, analyze_idle_timeout,
    analyze_idle_timeout_from_env,
};
pub use peer::{ensure_peer_is_self, peer_pid, peer_uid, self_uid};
pub use probe::{SocketVerdict, probe_socket_verdict, socket_is_serving};
pub use retry::ConnectRetry;
pub use rpc::{
    MAX_FRAME_BYTES, UdsRpcError, encode_frame, send_framed_notification, send_framed_request,
    send_framed_request_capped, send_framed_request_retrying, write_frame,
};
pub use singleton::bind_singleton_hardened;
pub use sockbuf::{
    SOCKET_BUFFER_BYTES, SocketBufferOutcome, SocketBufferSizes, socket_buffer_sizes,
    tune_connected_buffers, tune_listener_buffers,
};
pub use stream_client::{
    FramedStream, send_framed_stream_request, send_framed_stream_request_capped,
};
#[cfg(feature = "uds-supervisor")]
pub use supervisor::{
    ServiceTimeouts, SpawnSpec, SupervisorConfig, SupervisorError, UdsServiceSupervisor,
};

use std::fs::Permissions;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};

use tokio::net::{UnixListener, UnixStream};

/// Mode every socket-bearing directory is created at and verified against.
///
/// Owner-only `rwx`. Unix path resolution needs search (`x`) permission on
/// every directory component, so a `0700` directory makes every socket inside
/// it unreachable to any other uid regardless of the socket's own mode.
pub const SOCKET_DIR_MODE: u32 = 0o700;

/// Mode every bound socket is set to before its first `accept`.
pub const SOCKET_MODE: u32 = 0o600;

/// Failures that mean a socket could not be made private, or could not be
/// trusted. Every variant is fatal to the operation — none is a "log and
/// continue" condition, because continuing would use a socket wider than the
/// ADRs promise.
///
/// `#[non_exhaustive]`: this enum gained two variants across two review rounds
/// of #5099 alone, so it will keep growing as the checks tighten. The attribute
/// costs nothing while `trusty-common` sits unpublished at 0.30.0 against a
/// published 0.28.1, and stops being free the moment 0.30.0 ships — after which
/// each new variant would be a breaking change. On an enum it constrains
/// *matching* only (external crates need a wildcard arm); it does not bar
/// constructing variants, which is the separate effect the attribute has on a
/// struct. Matching this exhaustively from outside `trusty-common` was never
/// possible anyway — every consumer converts it (`io::Error::other`, `?` into
/// `anyhow`, or `Display` in a log) rather than inspecting variants.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum UdsSecurityError {
    /// The socket path is a bare filename with no directory component, so
    /// there is nothing to harden.
    #[error("socket path {path} has no parent directory to harden")]
    NoParent {
        /// The offending socket path.
        path: PathBuf,
    },

    /// The socket path does not fit the kernel's `sockaddr_un.sun_path` buffer.
    #[error(
        "socket path is {len} bytes but the kernel's sun_path buffer holds {capacity} \
         (including its NUL terminator): {path}"
    )]
    PathTooLong {
        /// The path that overflowed.
        path: PathBuf,
        /// Length of `path` in bytes.
        len: usize,
        /// Bytes available in `sockaddr_un.sun_path` on this platform.
        capacity: usize,
    },

    /// Creating the socket directory failed.
    #[error("create socket directory {path}: {source}")]
    CreateDir {
        /// Directory that could not be created.
        path: PathBuf,
        /// Underlying OS error.
        #[source]
        source: std::io::Error,
    },

    /// The socket directory is a symlink. Refused outright: `metadata` and
    /// `set_permissions` follow links, so verifying a symlinked directory reads
    /// and chmods the wrong inode (#5099 review finding 1).
    #[error("socket directory {path} is a symlink; refusing to trust it")]
    SymlinkDir {
        /// The symlinked path.
        path: PathBuf,
    },

    /// The socket-directory path exists but is not a directory. Refused before
    /// anything chmods it — a regular file here used to be narrowed to `0700`
    /// on its way to a confusing `ENOTDIR` from `bind` (#5099 review round 2).
    #[error("socket directory {path} is a {found}, not a directory")]
    NotADirectory {
        /// The offending path.
        path: PathBuf,
        /// What `lstat` actually found there.
        found: String,
    },

    /// The directory already existed but belongs to a different uid. Repairing
    /// its mode would not make it safe — another user controls its contents —
    /// so this fails closed instead.
    #[error("socket directory {path} is owned by uid {owner}, not {expected}")]
    ForeignDirOwner {
        /// Directory whose ownership was rejected.
        path: PathBuf,
        /// uid that actually owns it.
        owner: u32,
        /// uid this process runs as.
        expected: u32,
    },

    /// The directory existed at a wider mode and could not be narrowed.
    #[error("narrow socket directory {path} to {mode:04o}: {source}")]
    HardenDir {
        /// Directory that could not be narrowed.
        path: PathBuf,
        /// Mode that was being applied.
        mode: u32,
        /// Underlying OS error.
        #[source]
        source: std::io::Error,
    },

    /// Another process is already serving the socket a singleton bind wanted.
    ///
    /// Not a takeover candidate: two listeners on one path means the kernel
    /// picks which one each delivery reaches, so the second must fail rather
    /// than unlink a live owner's socket (#5182).
    #[error("another process is already serving {path}")]
    AlreadyServing {
        /// Socket that is already owned.
        path: PathBuf,
    },

    /// The socket path exists but holds something that is not a socket, so a
    /// singleton bind refused it instead of unlinking it.
    ///
    /// Why its own variant rather than [`UdsSecurityError::AlreadyServing`]:
    /// the two say different things to an operator — one means "another daemon
    /// has this path", the other "something unrelated is sitting on it" — and
    /// on macOS the second used to arrive dressed as the first, by accident of
    /// which errno a connect to a regular file returns. See #7312.
    #[error("socket path {path} is a {found}, not a socket; refusing to remove it")]
    NotASocketFile {
        /// The offending path.
        path: PathBuf,
        /// What `lstat` actually found there.
        found: String,
    },

    /// `UnixListener::bind` failed.
    #[error("bind unix socket at {path}: {source}")]
    Bind {
        /// Socket path that could not be bound.
        path: PathBuf,
        /// Underlying OS error.
        #[source]
        source: std::io::Error,
    },

    /// The socket bound but could not be narrowed to [`SOCKET_MODE`].
    #[error("narrow socket {path} to {mode:04o}: {source}")]
    Chmod {
        /// Socket that could not be narrowed.
        path: PathBuf,
        /// Mode that was being applied.
        mode: u32,
        /// Underlying OS error.
        #[source]
        source: std::io::Error,
    },

    /// A client refused to dial a socket that failed verification.
    #[error("refusing to connect to {path}: {reason}")]
    UntrustedSocket {
        /// The socket that was refused.
        path: PathBuf,
        /// Which property failed.
        reason: String,
    },

    /// Could not `lstat` a path while verifying it ahead of a connect. Distinct
    /// from [`UdsSecurityError::CreateDir`]: a dialer creates nothing, so
    /// reporting a creation failure there named an action never attempted
    /// (#5099 review round 2, finding 2).
    #[error("stat {path} while verifying it for connect: {source}")]
    StatForConnect {
        /// Path that could not be stat'd.
        path: PathBuf,
        /// Underlying OS error.
        #[source]
        source: std::io::Error,
    },

    /// `UnixStream::connect` failed.
    #[error("connect to unix socket at {path}: {source}")]
    Connect {
        /// Socket path that could not be dialled.
        path: PathBuf,
        /// Underlying OS error.
        #[source]
        source: std::io::Error,
    },

    /// A socket buffer could not be sized to
    /// [`sockbuf::SOCKET_BUFFER_BYTES`] (#6896).
    ///
    /// Fatal by design: a socket left at the platform default still works, at
    /// the round-trip cost `sockbuf` exists to remove and with nothing saying
    /// so. See that module's docs.
    #[error("size {option} to {requested} bytes: {source}")]
    SocketBuffer {
        /// Which option failed — `SO_SNDBUF` or `SO_RCVBUF`.
        option: &'static str,
        /// Bytes that were requested.
        requested: usize,
        /// Underlying OS error.
        #[source]
        source: std::io::Error,
    },

    /// A socket buffer could not be READ back (#6896 review).
    ///
    /// Distinct from [`UdsSecurityError::SocketBuffer`]: reporting a
    /// `getsockopt` failure through that variant printed "size SO_SNDBUF to
    /// 1048576 bytes", naming a write this branch never attempted.
    #[error("read {option} back: {source}")]
    SocketBufferRead {
        /// Which option could not be read — `SO_SNDBUF` or `SO_RCVBUF`.
        option: &'static str,
        /// Underlying OS error.
        #[source]
        source: std::io::Error,
    },

    /// Reading the connected peer's credentials failed.
    #[error("read peer credentials: {source}")]
    PeerCred {
        /// Underlying OS error.
        #[source]
        source: std::io::Error,
    },

    /// The connected peer runs as a different uid.
    #[error("refused connection from uid {peer}; this process runs as uid {expected}")]
    ForeignPeer {
        /// uid on the other end of the connection.
        peer: u32,
        /// uid this process runs as.
        expected: u32,
    },

    /// No peer-credential syscall is wired up for this target.
    #[error("peer-credential checks are not implemented for this platform")]
    UnsupportedPlatform,
}

impl UdsSecurityError {
    /// Whether this is the errno a torn-down socket buffer produces (#6896).
    ///
    /// Why: macOS answers `EINVAL` to `setsockopt(SO_SNDBUF)` once a socket's
    /// peer has hung up. That is one of the two signals
    /// [`sockbuf::hangup_is_benign`] requires before it treats an unsized
    /// socket as harmless rather than as a failure — see that module's docs for
    /// why one signal is not enough.
    ///
    /// Test: `tune_connected_buffers_reports_a_peer_that_already_hung_up`.
    pub(crate) fn is_disconnected_socket(&self) -> bool {
        matches!(
            self,
            Self::SocketBuffer { source, .. } if source.raw_os_error() == Some(libc::EINVAL)
        )
    }
}

/// Bytes available in this platform's `sockaddr_un.sun_path`, NUL included.
///
/// Why: 104 on macOS, 108 on Linux. Rust rejects an over-long path before the
/// syscall with a bare `invalid argument` that names neither the limit nor the
/// offending path, which is a poor diagnostic for a value assembled from
/// `$TMPDIR` plus a palace name (#5099 review finding 4).
/// What: derived from the actual struct layout rather than hardcoded.
/// Test: `sun_path_capacity_is_platform_plausible`.
pub fn sun_path_capacity() -> usize {
    size_of::<libc::sockaddr_un>() - std::mem::offset_of!(libc::sockaddr_un, sun_path)
}

/// Reject a socket path that cannot fit the kernel's address buffer.
///
/// Why: turns `invalid argument` into a message naming the budget, the actual
/// length, and the path — the difference between "the daemon is broken" and
/// "this palace name is 12 characters too long".
/// What: compares the path's byte length against [`sun_path_capacity`], leaving
/// room for the NUL terminator.
/// Test: `check_sun_path_budget_accepts_a_path_that_fits` and
/// `check_sun_path_budget_rejects_an_over_long_path`.
pub fn check_sun_path_budget(path: &Path) -> Result<(), UdsSecurityError> {
    let len = path.as_os_str().as_encoded_bytes().len();
    let capacity = sun_path_capacity();
    if len >= capacity {
        return Err(UdsSecurityError::PathTooLong {
            path: path.to_path_buf(),
            len,
            capacity,
        });
    }
    Ok(())
}

/// Per-uid directory under the system scratch space for daemon sockets.
///
/// Why: the convention this replaces was `$TMPDIR`, falling back to `/tmp`. On
/// macOS `$TMPDIR` is a per-user `/var/folders/…/T/` that is already `0700`, so
/// the fallback never bit there. On a Linux host with `TMPDIR` unset it
/// resolved to `/tmp` — mode `1777`, owned by root — which can be neither
/// narrowed to `0700` nor trusted, so any socket in it was reachable by every
/// local user. Interposing a uid-keyed subdirectory gives a directory this
/// process owns and can hold at `0700` on both platforms uniformly.
///
/// What: `<$TMPDIR or /tmp>/trusty-<uid>`. The name is kept short on purpose:
/// `sun_path` is 104 bytes on macOS and macOS' `$TMPDIR` already consumes
/// roughly half of that, so every byte here comes out of the budget available
/// to palace names (see `trusty-memory`'s `bm25_supervisor_concurrency` tests).
///
/// Test: `scratch_socket_dir_is_uid_keyed`; the `$TMPDIR` resolution rules are
/// asserted against [`scratch_socket_dir_from`] so no test has to mutate the
/// process-global `TMPDIR` (which reddens unrelated sibling tests — see the
/// `trusty-mpm` `tmpdir-cross-test-pollution` fragment).
pub fn scratch_socket_dir() -> PathBuf {
    scratch_socket_dir_from(std::env::var("TMPDIR").ok().as_deref(), self_uid())
}

/// [`scratch_socket_dir`] with its two environment inputs passed explicitly.
///
/// Why: keeps the resolution rules unit-testable without `set_var`. A test that
/// mutates `TMPDIR` is visible to every concurrently-running sibling in the
/// same test binary, and `tempfile` honors it.
/// What: treats an absent, empty, or whitespace-only `tmpdir` as `/tmp`.
/// Test: `scratch_socket_dir_from_uses_tmpdir_when_set`,
/// `scratch_socket_dir_from_falls_back_to_tmp`.
pub fn scratch_socket_dir_from(tmpdir: Option<&str>, uid: u32) -> PathBuf {
    let base = match tmpdir {
        Some(p) if !p.trim().is_empty() => PathBuf::from(p),
        _ => PathBuf::from("/tmp"),
    };
    base.join(format!("trusty-{uid}"))
}

/// Resolve a socket path's parent, rejecting a bare filename.
///
/// `Path::parent` yields `Some("")` — not `None` — for a bare filename, so the
/// empty case has to be filtered explicitly or a relative socket name would be
/// treated as living in an unhardened cwd.
fn socket_parent(path: &Path) -> Result<&Path, UdsSecurityError> {
    path.parent()
        .filter(|p| !p.as_os_str().is_empty())
        .ok_or_else(|| UdsSecurityError::NoParent {
            path: path.to_path_buf(),
        })
}

/// Bind a listener at `path` with its directory at `0700` and the socket at
/// `0600`.
///
/// Why: the single entry point every daemon binds through, so the permission
/// contract cannot drift between the four bind sites that previously each
/// called `UnixListener::bind` bare (`trusty-embedderd`, `trusty-bm25-daemon`,
/// and two in `trusty-agents`). See #5099.
///
/// What: checks the `sun_path` budget, hardens the parent directory via
/// [`prepare_socket_dir`], binds, then narrows the socket to [`SOCKET_MODE`]
/// before returning — so the listener is already `0600` when the caller first
/// calls `accept`. Deliberately does *not* remove a stale socket file:
/// `CtrlSocket::bind_singleton` must probe before clobbering, and folding an
/// unconditional unlink in here would break that singleton guarantee. Callers
/// that want stale-file cleanup keep doing it themselves, immediately before
/// this call.
///
/// The `0600` step is defence in depth, not the race fix — the `0700` directory
/// is what makes the socket unreachable during the window between `bind` and
/// `chmod`. [`prepare_socket_dir`]'s docs carry the reasoning and the residual
/// race it does not close.
///
/// # Errors
///
/// Any [`UdsSecurityError`] the directory hardening, the bind, the chmod or
/// [`sockbuf::tune_listener_buffers`] produces.
///
/// Test: `bind_hardened_sets_socket_0600_and_dir_0700`,
/// `bind_hardened_socket_is_connectable_after_hardening`,
/// `bind_hardened_rejects_an_over_long_path`,
/// `hardened_sockets_hold_far_more_in_flight_than_the_platform_default`.
pub fn bind_hardened(path: &Path) -> Result<UnixListener, UdsSecurityError> {
    check_sun_path_budget(path)?;
    prepare_socket_dir(socket_parent(path)?)?;

    let listener = UnixListener::bind(path).map_err(|source| UdsSecurityError::Bind {
        path: path.to_path_buf(),
        source,
    })?;

    std::fs::set_permissions(path, Permissions::from_mode(SOCKET_MODE)).map_err(|source| {
        UdsSecurityError::Chmod {
            path: path.to_path_buf(),
            mode: SOCKET_MODE,
            source,
        }
    })?;

    // #6896: sized on the LISTENER, which macOS copies onto every socket
    // `accept` returns. Linux does not, so the accepted end is sized by
    // `accept_sized`. The strict form: a listener has no peer, so it must never
    // reach the hung-up-peer outcome (#6896 review).
    sockbuf::tune_listener_buffers(&listener)?;

    Ok(listener)
}

/// Accept one connection and size its buffers.
///
/// Why: an accepted socket does not carry the listener's `SO_SNDBUF` /
/// `SO_RCVBUF` on Linux. AF_UNIX builds the server-side socket from scratch in
/// `unix_stream_connect`, so it comes back at `net.core.wmem_default` /
/// `rmem_default` however the listener was sized; only macOS copies the
/// listener's sizing, in `sonewconn`. #6896 sized the listener alone and stated
/// the copy as universal, which left every server-side socket on Linux at the
/// platform default — paying, on one end of every connection, exactly the round
/// trips that issue exists to remove.
///
/// What: `accept`, then [`sockbuf::tune_connected_buffers`] on what came back.
/// The forgiving form, because an accepted socket can already have lost its
/// peer: [`probe::socket_is_serving`] connects and closes without sending
/// anything, and that probe has to stay a probe.
///
/// A sizing failure is logged and the connection returned anyway — the one
/// place in this module a failure is not propagated. The connection is already
/// accepted and cannot be un-accepted, so refusing it would turn a throughput
/// knob into an availability failure. [`bind_hardened`] and
/// [`connect_hardened`] propagate because their caller still has somewhere else
/// to go.
///
/// This does NOT check the peer's uid; [`ensure_peer_is_self`] stays a separate
/// call, made once per connection where the connection is served.
///
/// # Errors
///
/// Whatever `UnixListener::accept` returns. Sizing never fails the call.
///
/// Test: `accept_sized_raises_the_accepted_socket_to_the_listeners_sizing`.
pub async fn accept_sized(
    listener: &UnixListener,
) -> std::io::Result<(UnixStream, tokio::net::unix::SocketAddr)> {
    let accepted = listener.accept().await?;
    // #6896: Linux hands back a default-sized socket here whatever the listener
    // carries, so both platforms need this to end up at the intended sizing.
    if let Err(e) = sockbuf::tune_connected_buffers(&accepted.0) {
        // #6896: name the listener, matching `drain_shutdown`'s precedent
        // (#6601 review) — under a persistent failure this warn repeats per
        // connection, and an unnamed socket leaves no way to tell which one.
        let socket = listener
            .local_addr()
            .ok()
            .and_then(|addr| addr.as_pathname().map(|p| p.display().to_string()))
            .unwrap_or_else(|| "<unknown>".to_string());
        tracing::warn!(
            %socket,
            error = %e,
            "accepted connection left at the platform socket buffer size"
        );
    }
    Ok(accepted)
}

/// Verify a socket and its directory, then connect.
///
/// Why: hardening only the bind side leaves the client trusting whatever is at
/// the path. A daemon that predates this change, or a socket an attacker
/// planted, still answers — and the supervisor's adopt-an-existing-socket path
/// means the daemon's own `ForeignDirOwner` check may never run, because it
/// never spawns when something is already listening (#5099 review finding 3).
/// The dialer has to do its own verification.
///
/// What: refuses unless the parent directory is a non-symlink `0700` directory
/// owned by this uid, and the socket itself is a non-symlink socket owned by
/// this uid at mode `0600`. Then connects.
///
/// This is a check-then-use, so a sufficiently privileged attacker could swap
/// the target between the `lstat` and the `connect`. It is not trying to win
/// that race — it is closing the case that actually occurs, where a wrong-mode
/// or wrong-owner socket persists and would otherwise be dialled silently.
///
/// # Errors
///
/// Any [`UdsSecurityError`] the verification, the connect or
/// [`sockbuf::tune_connected_buffers`] produces.
///
/// Test: `connect_hardened_accepts_a_properly_hardened_socket`,
/// `connect_hardened_refuses_a_world_readable_socket`,
/// `connect_hardened_refuses_a_socket_in_a_wide_directory`,
/// `connect_hardened_refuses_a_regular_file`,
/// `hardened_sockets_hold_far_more_in_flight_than_the_platform_default`.
pub async fn connect_hardened(path: &Path) -> Result<UnixStream, UdsSecurityError> {
    check_sun_path_budget(path)?;
    verify_socket_for_connect(path)?;

    let stream = UnixStream::connect(path)
        .await
        .map_err(|source| UdsSecurityError::Connect {
            path: path.to_path_buf(),
            source,
        })?;

    // #6896: the dialling end of the pair; the accepting end inherits the
    // listener's sizing. The forgiving form — the server may have dropped this
    // connection already, which is not a failure to report.
    sockbuf::tune_connected_buffers(&stream)?;

    Ok(stream)
}

/// The filesystem half of [`connect_hardened`], split out so it is testable
/// without standing up a listener.
///
/// Test: the `connect_hardened_*` tests plus `verify_socket_for_connect_*`.
pub fn verify_socket_for_connect(path: &Path) -> Result<(), UdsSecurityError> {
    let own = self_uid();
    let refuse = |reason: String| UdsSecurityError::UntrustedSocket {
        path: path.to_path_buf(),
        reason,
    };

    let parent = socket_parent(path)?;
    let dmeta =
        std::fs::symlink_metadata(parent).map_err(|source| UdsSecurityError::StatForConnect {
            path: parent.to_path_buf(),
            source,
        })?;
    if dmeta.file_type().is_symlink() {
        return Err(UdsSecurityError::SymlinkDir {
            path: parent.to_path_buf(),
        });
    }
    if !dmeta.file_type().is_dir() {
        return Err(UdsSecurityError::NotADirectory {
            path: parent.to_path_buf(),
            found: dir::describe_file_type(&dmeta.file_type()).to_string(),
        });
    }
    if dmeta.uid() != own {
        return Err(UdsSecurityError::ForeignDirOwner {
            path: parent.to_path_buf(),
            owner: dmeta.uid(),
            expected: own,
        });
    }
    let dmode = dmeta.permissions().mode() & 0o777;
    if dmode != SOCKET_DIR_MODE {
        return Err(refuse(format!(
            "containing directory {} is mode {dmode:04o}, not {SOCKET_DIR_MODE:04o}",
            parent.display()
        )));
    }

    let smeta =
        std::fs::symlink_metadata(path).map_err(|source| UdsSecurityError::StatForConnect {
            path: path.to_path_buf(),
            source,
        })?;
    let ftype = smeta.file_type();
    if ftype.is_symlink() {
        return Err(refuse("socket path is a symlink".to_string()));
    }
    if !std::os::unix::fs::FileTypeExt::is_socket(&ftype) {
        return Err(refuse("path is not a socket".to_string()));
    }
    if smeta.uid() != own {
        return Err(refuse(format!(
            "socket is owned by uid {}, not {own}",
            smeta.uid()
        )));
    }
    let smode = smeta.permissions().mode() & 0o777;
    if smode != SOCKET_MODE {
        return Err(refuse(format!(
            "socket is mode {smode:04o}, not {SOCKET_MODE:04o}"
        )));
    }
    Ok(())
}