1use std::fs::{File, OpenOptions};
4use std::io;
5use std::os::fd::AsRawFd;
6use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt};
7use std::path::{Path, PathBuf};
8
9use super::{LaneHold, ShmAccessPolicy, ShmRegion, ShmRegionLock, WatcherHold};
10
11#[derive(Clone, Debug, Eq, PartialEq, Hash)]
18pub struct GroupAccess {
19 owner: u32,
20 gid: u32,
21 directory: PathBuf,
22}
23
24impl GroupAccess {
25 pub fn new(
26 owner: u32,
27 gid: u32,
28 directory: impl AsRef<Path>,
29 ) -> io::Result<Self> {
30 let directory = directory.as_ref();
31 if owner == !0 || gid == !0 || !directory.is_absolute() {
32 return Err(io::Error::new(
33 io::ErrorKind::InvalidInput,
34 "explicit owner, group and absolute IPC directory required",
35 ));
36 }
37 let leaf = directory.file_name().ok_or_else(|| {
38 io::Error::new(
39 io::ErrorKind::InvalidInput,
40 "IPC directory must not be a filesystem root",
41 )
42 })?;
43 let parent = directory.parent().unwrap().canonicalize()?;
44 Ok(Self { owner, gid, directory: parent.join(leaf) })
46 }
47
48 pub fn owner_uid(&self) -> u32 {
49 self.owner
50 }
51 pub fn gid(&self) -> u32 {
52 self.gid
53 }
54 pub fn directory(&self) -> &Path {
55 &self.directory
56 }
57 pub fn is_owner(&self) -> bool {
58 (unsafe { libc::geteuid() }) == self.owner
59 }
60
61 pub fn segment_name(
65 &self,
66 fleet: &str,
67 kind: u8,
68 ) -> String {
69 let mut hash = blake3::Hasher::new();
70 hash.update(b"orbit/group-resource/v1\0");
71 hash.update(&self.owner.to_le_bytes());
72 hash.update(&self.gid.to_le_bytes());
73 hash.update(&[kind]);
74 let directory = self.directory.as_os_str().as_encoded_bytes();
75 hash.update(&(directory.len() as u64).to_le_bytes());
76 hash.update(directory);
77 hash.update(fleet.as_bytes());
78 format!("/orbit-{}", &hash.finalize().to_hex()[..24])
79 }
80
81 pub fn prepare(&self) -> io::Result<()> {
83 self.open_directory(&self.directory, 0o750)?;
84 self.open_directory(&self.directory.join("bells"), 0o2770)?;
85 Ok(())
86 }
87
88 fn open_directory(
89 &self,
90 path: &Path,
91 mode: u32,
92 ) -> io::Result<File> {
93 let created = if self.is_owner() {
94 match std::fs::DirBuilder::new().mode(0o700).create(path) {
95 Ok(()) => true,
96 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => false,
97 Err(e) => return Err(e),
98 }
99 } else {
100 false
101 };
102 let file = OpenOptions::new()
103 .read(true)
104 .custom_flags(
105 libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
106 )
107 .open(path)?;
108 if created {
109 self.configure(&file, mode)?;
110 }
111 let m = file.metadata()?;
112 if !m.is_dir()
113 || m.uid() != self.owner
114 || m.gid() != self.gid
115 || m.mode() & 0o7777 != mode
116 {
117 return Err(denied("IPC directory owner/group/mode mismatch"));
118 }
119 Ok(file)
120 }
121
122 fn configure(
123 &self,
124 file: &File,
125 mode: u32,
126 ) -> io::Result<()> {
127 if unsafe { libc::fchown(file.as_raw_fd(), !0, self.gid) } != 0 {
128 return Err(io::Error::last_os_error());
129 }
130 if unsafe { libc::fchmod(file.as_raw_fd(), mode as libc::mode_t) } != 0
131 {
132 return Err(io::Error::last_os_error());
133 }
134 Ok(())
135 }
136
137 fn lock_file(
138 &self,
139 name: &str,
140 ) -> io::Result<std::os::fd::OwnedFd> {
141 use std::ffi::CString;
142 use std::os::fd::{FromRawFd, OwnedFd};
143 if name.is_empty() || name.contains('/') || name == "." || name == ".."
144 {
145 return Err(io::Error::new(
146 io::ErrorKind::InvalidInput,
147 "invalid IPC lock name",
148 ));
149 }
150 let directory = self.open_directory(&self.directory, 0o750)?;
151 let name = CString::new(name)
152 .map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
153 let mut created = false;
154 let flags = libc::O_RDWR | libc::O_CLOEXEC | libc::O_NOFOLLOW;
155 let raw = if self.is_owner() {
156 let fd = unsafe {
157 libc::openat(
158 directory.as_raw_fd(),
159 name.as_ptr(),
160 flags | libc::O_CREAT | libc::O_EXCL,
161 0o600 as libc::c_uint,
162 )
163 };
164 if fd >= 0 {
165 created = true;
166 fd
167 } else if io::Error::last_os_error().raw_os_error()
168 == Some(libc::EEXIST)
169 {
170 unsafe {
171 libc::openat(directory.as_raw_fd(), name.as_ptr(), flags)
172 }
173 } else {
174 return Err(io::Error::last_os_error());
175 }
176 } else {
177 unsafe { libc::openat(directory.as_raw_fd(), name.as_ptr(), flags) }
178 };
179 if raw < 0 {
180 return Err(io::Error::last_os_error());
181 }
182 let file = File::from(unsafe { OwnedFd::from_raw_fd(raw) });
183 if created {
184 self.configure(&file, 0o660)?;
185 }
186 let m = file.metadata()?;
187 if !m.is_file()
188 || m.uid() != self.owner
189 || m.gid() != self.gid
190 || m.mode() & 0o7777 != 0o660
191 || m.nlink() != 1
192 {
193 return Err(denied("IPC lock owner/group/mode/type mismatch"));
194 }
195 Ok(file.into())
196 }
197
198 pub fn prepare_locks(
200 &self,
201 segment: &str,
202 lanes: usize,
203 ) -> io::Result<()> {
204 self.prepare()?;
205 for lane in 0..lanes {
206 self.lock_file(&format!(
207 "{}.lane{lane}.lock",
208 segment.trim_start_matches('/')
209 ))?;
210 self.lock_file(&format!(
211 "{}.watch{lane}.lock",
212 segment.trim_start_matches('/')
213 ))?;
214 }
215 Ok(())
216 }
217
218 pub fn open_region(
219 &self,
220 name: &str,
221 size: usize,
222 ) -> io::Result<(ShmRegion, ShmRegionLock)> {
223 self.prepare()?;
224 let fd =
225 self.lock_file(&format!("{}.lock", name.trim_start_matches('/')))?;
226 let lock = super::lock_fd_exclusive(fd)?;
227 let policy = ShmAccessPolicy::GroupReadWrite { gid: self.gid };
228 let region = if self.is_owner() {
229 ShmRegion::open_or_create_with_policy(name, size, policy)?
230 } else {
231 ShmRegion::open_existing_read_write(name, size, self.owner, policy)?
232 };
233 let result = self.verify_shared_mode(name);
236 if let Err(error) = result {
237 if region.created() {
238 let _ = unsafe { libc::shm_unlink(region.name.as_ptr()) };
241 }
242 return Err(error);
243 }
244 Ok((region, lock))
245 }
246
247 pub fn unlink_region(
251 &self,
252 region: &ShmRegion,
253 ) -> io::Result<()> {
254 if !self.is_owner() {
255 return Err(denied("only the group resource owner may unlink"));
256 }
257 self.verify_shared_mode(
258 region
259 .name
260 .to_str()
261 .map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?,
262 )?;
263 if unsafe { libc::shm_unlink(region.name.as_ptr()) } != 0 {
264 return Err(io::Error::last_os_error());
265 }
266 Ok(())
267 }
268
269 fn verify_shared_mode(
270 &self,
271 name: &str,
272 ) -> io::Result<()> {
273 use std::os::fd::{FromRawFd, OwnedFd};
274 let name = std::ffi::CString::new(name)
275 .map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
276 let raw = unsafe { libc::shm_open(name.as_ptr(), libc::O_RDONLY, 0) };
277 if raw < 0 {
278 return Err(io::Error::last_os_error());
279 }
280 let file = File::from(unsafe { OwnedFd::from_raw_fd(raw) });
281 let m = file.metadata()?;
282 if m.uid() != self.owner
283 || m.gid() != self.gid
284 || m.mode() & 0o7777 != 0o660
285 {
286 return Err(denied(
287 "shared SHM requires exact owner/group and mode 0660; check the owner's creation mask",
288 ));
289 }
290 Ok(())
291 }
292
293 pub fn try_hold_lane(
294 &self,
295 segment: &str,
296 lane: usize,
297 ) -> io::Result<Option<LaneHold>> {
298 Ok(self
299 .try_hold(&format!(
300 "{}.lane{lane}.lock",
301 segment.trim_start_matches('/')
302 ))?
303 .map(|lock_fd| LaneHold { lock_fd }))
304 }
305
306 pub fn try_hold_watcher(
307 &self,
308 segment: &str,
309 slot: usize,
310 ) -> io::Result<Option<WatcherHold>> {
311 Ok(self
312 .try_hold(&format!(
313 "{}.watch{slot}.lock",
314 segment.trim_start_matches('/')
315 ))?
316 .map(|lock_fd| WatcherHold { lock_fd }))
317 }
318
319 fn try_hold(
320 &self,
321 name: &str,
322 ) -> io::Result<Option<std::os::fd::OwnedFd>> {
323 let fd = self.lock_file(name)?;
324 if unsafe { libc::flock(fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) }
325 == 0
326 {
327 return Ok(Some(fd));
328 }
329 let e = io::Error::last_os_error();
330 if e.kind() == io::ErrorKind::WouldBlock { Ok(None) } else { Err(e) }
331 }
332
333 pub fn bell_path(
336 &self,
337 segment: &str,
338 suffix: &str,
339 ) -> PathBuf {
340 self.directory
341 .join("bells")
342 .join(format!("{}.{suffix}", segment.trim_start_matches('/')))
343 }
344
345 pub fn bind_bell(
346 &self,
347 path: &Path,
348 ) -> io::Result<std::os::unix::net::UnixDatagram> {
349 use std::os::unix::fs::FileTypeExt;
350 let expected = self.directory.join("bells");
351 if path.parent() != Some(expected.as_path()) {
352 return Err(denied("bell outside IPC directory"));
353 }
354 self.open_directory(&expected, 0o2770)?;
355 match std::fs::symlink_metadata(path) {
356 Ok(m) if m.file_type().is_socket() && m.gid() == self.gid => {
357 std::fs::remove_file(path)?
358 }
359 Ok(_) => {
360 return Err(denied("refusing to replace non-socket IPC bell"));
361 }
362 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
363 Err(e) => return Err(e),
364 }
365 let socket = std::os::unix::net::UnixDatagram::bind(path)?;
366 let m = std::fs::symlink_metadata(path)?;
369 if !m.file_type().is_socket()
370 || m.gid() != self.gid
371 || m.mode() & 0o020 == 0
372 {
373 return Err(denied(
374 "IPC socket group/write permission mismatch; check creation mask",
375 ));
376 }
377 socket.set_nonblocking(true)?;
378 Ok(socket)
379 }
380}
381
382fn denied(message: &'static str) -> io::Error {
383 io::Error::new(io::ErrorKind::PermissionDenied, message)
384}