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
//! Binding a socket exactly one process at a time may own (#5182).
//!
//! Why: a console-supervised child is spawned, killed, and respawned on demand,
//! and a child that dies without unlinking its socket leaves a file that makes
//! the next `bind` fail with `EADDRINUSE`. [`super::bind_hardened`] deliberately
//! does NOT unlink for it — folding an unconditional unlink in there would let
//! any process silently steal a socket another one is live on. So the
//! probe-then-take-over decision belongs in its own entry point, where the
//! condition it turns on is stated once.
//!
//! What: [`bind_singleton_hardened`] asks whether anything is actually
//! answering. If something is, it refuses rather than clobbering a live owner.
//! If nothing is, the file is a corpse: it is unlinked and rebound through
//! [`super::bind_hardened`], so the replacement is `0600` in a `0700` directory
//! exactly as a first bind would be.
//!
//! 🔴 The takeover fires ONLY on [`SocketVerdict::NotServing`], the verdict the
//! kernel actually proves (ENOENT / ECONNREFUSED), **and only when the path is
//! a socket to begin with**. An ambiguous probe —
//! [`SocketVerdict::Inconclusive`] — is treated as a live owner and refused.
//! The asymmetry is the point: refusing a dead socket costs one failed start
//! that a retry fixes, while unlinking a live one strands its owner on an
//! inode nothing can reach.
//!
//! 🔴 The file-type half of that rule is load-bearing and was missing until
//! #7312. Linux and macOS disagree about which errno a connect to a NON-socket
//! returns: macOS/BSD `unp_connect` answers `ENOTSOCK`, which classifies as
//! `Inconclusive` and refuses, while Linux's `unix_find_other` sets
//! `-ECONNREFUSED` before its `S_ISSOCK` test, which classifies as
//! `NotServing` and licensed an unlink. So on Linux a daemon pointed at a path
//! holding an ordinary file deleted that file and bound over it. The probe
//! cannot be made to answer this — `lstat` can, and does, first.
//! [`super::verify_socket_for_connect`] has always applied that rule on the
//! dialing side; this is its missing half on the binding side.
//!
//! The probe is a bare `connect`, not [`super::connect_hardened`], and that is
//! deliberate. The question here is only "is a process serving this path", and
//! a verification failure (wrong mode, wrong owner) does not answer it — a live
//! server with a wrong-mode socket would be misread as a corpse and unlinked
//! out from under itself.
//!
//! `trusty-agents`' `CtrlSocket::bind_singleton` predates this and still carries
//! its own copy; migrating it is a separate change, not a side effect of one
//! that adds two new bind sites.
//!
//! Test: `tests.rs` — `bind_singleton_*` and `takeover_verdict_*`.
use Path;
use Duration;
use UnixListener;
use ;
use ;
/// How long the takeover probe waits for an answer.
///
/// A local socket answers or refuses in microseconds, so a full second is not a
/// latency budget — it is headroom, so that `Inconclusive` means "the kernel
/// would not answer" rather than "this machine was busy". The supervisor's
/// 200 ms liveness probe runs per request and cannot afford that; this runs
/// once per process start and can. A starved 200 ms probe here read as
/// `Inconclusive` and refused a genuinely stale socket, which is safe but
/// wrong.
const PROBE_TIMEOUT: Duration = from_secs;
/// What [`bind_singleton_hardened`] must do about a path that already exists.
///
/// Why a named decision rather than an inline `match`: the two refusal arms are
/// awkward to reach through real syscalls — `Inconclusive` needs a socket whose
/// connect neither completes nor is refused, and `NotASocket` needs a kernel
/// that reports the file type through the errno, which macOS does and Linux
/// does not. Separating the decision from the syscalls makes every arm testable
/// unprivileged on either platform, the same reason
/// [`super::dir::DirVerdict`] exists.
///
/// Test: the `takeover_verdict_*` tests in `tests.rs`.
pub
/// Decide whether an existing path may be unlinked and rebound.
///
/// Why: see [`TakeoverVerdict`]. The file type is checked FIRST and outranks
/// the probe, because on Linux the probe's answer for a non-socket is
/// indistinguishable from its answer for a dead socket (#7312).
/// What: `NotASocket` whenever `is_socket` is false, whatever the probe said;
/// otherwise `TakeOver` on the one verdict the kernel proved, `Occupied` on
/// every other. `verdict` is `None` when the caller skipped the probe because
/// the path is not a socket — a non-socket occupant costs no `connect`, since
/// the errno one would return is the very thing that misread it.
/// Test: `takeover_verdict_refuses_a_non_socket_even_when_the_probe_says_dead`,
/// `takeover_verdict_refuses_a_non_socket_that_was_never_probed`,
/// `takeover_verdict_takes_over_a_dead_socket`,
/// `takeover_verdict_refuses_a_served_socket`,
/// `takeover_verdict_refuses_an_inconclusive_probe`.
pub
/// Bind `path`, taking over a socket file no process is serving.
///
/// # Errors
///
/// [`UdsSecurityError::AlreadyServing`] when another process answers the path,
/// or when the probe cannot settle the question — a caller must not proceed,
/// because two listeners on one socket means every delivery goes to whichever
/// the kernel picks. [`UdsSecurityError::NotASocketFile`] when the path holds
/// something that is not a socket, which this function refuses rather than
/// deletes (#7312). Otherwise any [`super::bind_hardened`] error.
///
/// Test: `bind_singleton_takes_over_a_stale_socket_file`,
/// `bind_singleton_refuses_a_socket_someone_is_serving`,
/// `bind_singleton_refuses_a_regular_file_and_leaves_it_on_disk`,
/// `bind_singleton_hardened_refuses_a_symlink_to_a_dead_socket`,
/// `bind_singleton_hardened_refuses_a_symlink_to_a_live_socket`,
/// `bind_singleton_binds_a_fresh_path`.
pub async