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
//
// Syd: rock-solid application kernel
// src/kernel/net/bind.rs: bind(2) handler
//
// Copyright (c) 2023, 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0
use std::{net::IpAddr, os::fd::AsRawFd};
use ipnet::IpNet;
use libseccomp::ScmpNotifResp;
use nix::{
errno::Errno,
sys::socket::{getsockname, SockaddrLike, SockaddrStorage},
};
use crate::{
cache::UnixVal,
compat::{sockaddr_family, AddressFamily},
cookie::{safe_bind, safe_fchdir, safe_umask},
fd::SafeOwnedFd,
info,
lookup::CanonicalPath,
path::XPathBuf,
proc::proc_umask,
req::UNotifyEventRequest,
sandbox::{Action, AddressPattern, Capability, CidrRule},
wildmatch::MatchMethod,
};
pub(crate) fn handle_bind(
fd: SafeOwnedFd,
addr: (SockaddrStorage, SockaddrStorage),
root: Option<CanonicalPath>,
request: &UNotifyEventRequest,
allow_safe_bind: bool,
) -> Result<ScmpNotifResp, Errno> {
let req = request.scmpreq;
let (addr, argaddr) = addr;
// Prepare environment for UNIX domain sockets.
if addr.as_unix_addr().and_then(|addr| addr.path()).is_some() {
let mask = proc_umask(req.pid())?;
// Honour directory for too long sockets.
// Current working directory is per-thread here.
// We cannot resolve symlinks in root or we risk TOCTOU!
#[expect(clippy::disallowed_methods)]
let dirfd = root.as_ref().unwrap().dir();
safe_fchdir(dirfd)?;
// Honour process' umask:
// Umask is per-thread here.
safe_umask(mask);
}
// Record blocking call so it can get invalidated.
request.cache.add_sys_block(req, false)?;
// All done, call underlying system call.
// bind(2) doesn't follow symlinks in basename.
let result = safe_bind(&fd, &addr);
// Remove invalidation record.
request.cache.del_sys_block(req.id)?;
// Check errors after critical section.
result?;
// Handle trace/allow_safe_bind and bind_map.
// Ignore errors as bind has already succeeded.
//
// Configure sandbox:
// Remove and re-add the address so repeated binds to the same
// address cannot overflow the vector.
#[expect(clippy::cognitive_complexity)]
let _result = (|fd: SafeOwnedFd, request: &UNotifyEventRequest| -> Result<(), Errno> {
let (addr, port) = match sockaddr_family(&addr) {
AddressFamily::Unix => {
let addr = addr.as_unix_addr().ok_or(Errno::EINVAL)?;
let unix = match (addr.path(), addr.as_abstract()) {
(Some(_), _) => {
// Case 1: UNIX domain socket
//
// addr.path()=Some asserts root is Some.
#[expect(clippy::disallowed_methods)]
let unix = root.unwrap().take();
// Handle bind_map after successful bind(2) for UNIX sockets.
// We ignore errors because there's nothing we can do about them.
// We use original address structure for path for getsockname(2).
let _ = request.add_unix(
&fd,
request.scmpreq.pid(),
UnixVal {
addr: argaddr.as_unix_addr().copied(),
..UnixVal::default()
},
);
drop(fd); // Close our copy of the socket.
if !allow_safe_bind {
return Ok(());
}
unix
}
(_, Some(path)) => {
// Case 2: UNIX abstract socket
//
// Prefix UNIX abstract sockets with `@' before access check.
// Abstract socket names may contain embedded NUL bytes.
let mut unix = XPathBuf::from("@");
unix.append_bytes(path);
// Handle bind_map after successful bind for UNIX sockets.
// We ignore errors because there's nothing we can do
// about them.
// BindMap is only used for SO_PEERCRED for UNIX abstract sockets.
let _ = request.add_unix(&fd, request.scmpreq.pid(), UnixVal::default());
drop(fd); // Close our copy of the socket.
if !allow_safe_bind {
return Ok(());
}
unix
}
_ => {
// Case 3: unnamed UNIX socket.
let unix = if addr.len() as usize == size_of::<libc::sa_family_t>() {
// Autobind on abstract UNIX socket.
getsockname::<SockaddrStorage>(fd.as_raw_fd())?
.as_unix_addr()
.ok_or(Errno::EINVAL)?
.as_abstract()
.map(|path| {
// Prefix UNIX abstract sockets with `@' before access check.
// Abstract socket names may contain embedded NUL bytes.
let mut unix = XPathBuf::from("@");
unix.append_bytes(path);
unix
})
.ok_or(Errno::EINVAL)?
} else {
// Use dummy path `!unnamed' for unnamed UNIX sockets.
XPathBuf::from("!unnamed")
};
// Handle bind_map after successful bind for UNIX sockets.
// We ignore errors because there's nothing we can do
// about them.
// BindMap is only used for SO_PEERCRED for UNIX abstract sockets.
let _ = request.add_unix(&fd, request.scmpreq.pid(), UnixVal::default());
drop(fd); // Close our copy of the socket.
if !allow_safe_bind {
return Ok(());
}
unix
}
};
info!("ctx": "bind", "op": "allow_safe_bind",
"sys": "bind", "pid": request.scmpreq.pid().as_raw(), "unix": &unix,
"msg": format!("add rule `allow/net/connect+{unix}' after bind"));
let mut sandbox = request.get_mut_sandbox();
let acl = sandbox.get_acl_mut(Capability::CAP_NET_CONNECT);
if let Some(idx) = acl.iter().position(|(p, m, a)| {
*m == MatchMethod::Literal && *a == Action::Allow && p.is_equal(unix.as_bytes())
}) {
acl.remove(idx);
}
return acl.push_front((unix, MatchMethod::Literal, Action::Allow));
}
AddressFamily::Inet => {
if !allow_safe_bind {
return Ok(());
}
let addr = addr.as_sockaddr_in().ok_or(Errno::EINVAL)?;
let mut port = addr.port();
let addr = IpNet::new_assert(IpAddr::V4(addr.ip()), 32);
if port == 0 {
port = getsockname::<SockaddrStorage>(fd.as_raw_fd())?
.as_sockaddr_in()
.ok_or(Errno::EINVAL)?
.port();
}
drop(fd); // Close our copy of the socket.
(addr, port)
}
AddressFamily::Inet6 => {
if !allow_safe_bind {
return Ok(());
}
let addr = addr.as_sockaddr_in6().ok_or(Errno::EINVAL)?;
let mut port = addr.port();
let addr = addr.ip();
let addr = if let Some(addr) = addr.to_ipv4_mapped() {
IpNet::new_assert(IpAddr::V4(addr), 32)
} else {
IpNet::new_assert(IpAddr::V6(addr), 128)
};
if port == 0 {
port = getsockname::<SockaddrStorage>(fd.as_raw_fd())?
.as_sockaddr_in6()
.ok_or(Errno::EINVAL)?
.port();
}
drop(fd); // Close our copy of the socket.
(addr, port)
}
_ => return Ok(()),
};
let addr = AddressPattern {
addr,
port: port.into(),
};
info!("ctx": "bind", "op": "allow_safe_bind",
"sys": "bind", "pid": request.scmpreq.pid().as_raw(), "rule": &addr,
"msg": format!("add rule `allow/net/connect+{addr}' after bind"));
let rule = CidrRule {
act: Action::Allow,
cap: Capability::CAP_NET_CONNECT,
pat: addr,
};
let mut sandbox = request.get_mut_sandbox();
if let Some(idx) = sandbox.cidr_rules.iter().position(|r| *r == rule) {
sandbox.cidr_rules.remove(idx);
}
sandbox.cidr_rules.push_front(rule)?;
// 1. The sandbox lock will be released on drop here.
// 2. The socket fd will be closed on drop here.
Ok(())
})(fd, request);
Ok(request.return_syscall(0))
}