1use std::io;
14use std::mem;
15use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
16use std::os::unix::net::UnixListener;
17use std::ptr;
18use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
19use std::sync::{Arc, Mutex};
20use std::thread::{self, JoinHandle};
21
22use super::ring::CONFIG_SIZE;
23
24struct Client {
25 fd: OwnedFd,
26 ready: bool,
28}
29
30struct Shared {
31 clients: Mutex<Vec<Client>>,
32 ready_count: AtomicUsize,
33 stop: AtomicBool,
34}
35
36pub struct Server {
37 path: String,
38 shared: Arc<Shared>,
39 wake: OwnedFd,
40 thread: Option<JoinHandle<()>>,
41}
42
43impl Server {
44 pub fn bind(path: &str, config: [u8; CONFIG_SIZE], ring_fd: RawFd) -> io::Result<Self> {
49 use std::os::unix::fs::MetadataExt;
50 if let Some(dir) = std::path::Path::new(path).parent()
51 && !dir.as_os_str().is_empty()
52 {
53 std::fs::create_dir_all(dir)?;
54 }
55 match std::fs::symlink_metadata(path) {
56 Ok(meta) if meta.uid() != unsafe { libc::geteuid() } => {
57 return Err(io::Error::new(
58 io::ErrorKind::PermissionDenied,
59 format!("{path} belongs to uid {}, not this session", meta.uid()),
60 ));
61 }
62 Ok(_) => std::fs::remove_file(path)?,
63 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
64 Err(e) => return Err(e),
65 }
66 let listener = UnixListener::bind(path)?;
67 restrict_to_owner(path)?;
68 listener.set_nonblocking(true)?;
69 let wake_raw = unsafe { libc::eventfd(0, libc::EFD_CLOEXEC | libc::EFD_NONBLOCK) };
70 if wake_raw < 0 {
71 return Err(io::Error::last_os_error());
72 }
73 let wake = unsafe { OwnedFd::from_raw_fd(wake_raw) };
74 let shared = Arc::new(Shared {
75 clients: Mutex::new(Vec::new()),
76 ready_count: AtomicUsize::new(0),
77 stop: AtomicBool::new(false),
78 });
79 let t_shared = shared.clone();
80 let t_wake = wake.as_raw_fd();
81 let thread = thread::Builder::new()
82 .name("pixelflux-webcam-ctl".into())
83 .spawn(move || serve(listener, t_wake, config, ring_fd, t_shared))?;
84 Ok(Server {
85 path: path.to_string(),
86 shared,
87 wake,
88 thread: Some(thread),
89 })
90 }
91
92 pub fn path(&self) -> &str {
93 &self.path
94 }
95
96 pub fn client_count(&self) -> usize {
98 self.shared.ready_count.load(Ordering::Relaxed)
99 }
100
101 pub fn ring_doorbell(&self) {
104 let mut clients = self
105 .shared
106 .clients
107 .lock()
108 .unwrap_or_else(|e| e.into_inner());
109 let mut i = 0;
110 while i < clients.len() {
111 if !clients[i].ready {
112 i += 1;
113 continue;
114 }
115 let byte = [1u8];
116 let n = unsafe {
117 libc::send(
118 clients[i].fd.as_raw_fd(),
119 byte.as_ptr() as *const _,
120 1,
121 libc::MSG_DONTWAIT | libc::MSG_NOSIGNAL,
122 )
123 };
124 if n < 0 {
125 let err = io::Error::last_os_error();
126 match err.raw_os_error() {
127 Some(libc::EAGAIN) | Some(libc::EINTR) => {}
128 _ => {
129 clients.remove(i);
130 self.shared.ready_count.fetch_sub(1, Ordering::Relaxed);
131 continue;
132 }
133 }
134 }
135 i += 1;
136 }
137 }
138
139 fn wake_thread(&self) {
140 let one: u64 = 1;
141 unsafe { libc::write(self.wake.as_raw_fd(), &one as *const u64 as *const _, 8) };
142 }
143}
144
145impl Drop for Server {
146 fn drop(&mut self) {
149 self.shared.stop.store(true, Ordering::Release);
150 self.wake_thread();
151 if let Some(t) = self.thread.take() {
152 let _ = t.join();
153 }
154 self.shared
155 .clients
156 .lock()
157 .unwrap_or_else(|e| e.into_inner())
158 .clear();
159 self.shared.ready_count.store(0, Ordering::Relaxed);
160 let _ = std::fs::remove_file(&self.path);
161 }
162}
163
164fn send_config_with_fd(sock: RawFd, config: &[u8], fd: RawFd) -> io::Result<()> {
166 let mut iov = libc::iovec {
167 iov_base: config.as_ptr() as *mut libc::c_void,
168 iov_len: config.len(),
169 };
170 let space = unsafe { libc::CMSG_SPACE(mem::size_of::<RawFd>() as u32) } as usize;
171 let mut cbuf = vec![0u64; space.div_ceil(mem::size_of::<u64>())];
172 let mut msg: libc::msghdr = unsafe { mem::zeroed() };
173 msg.msg_iov = &mut iov;
174 msg.msg_iovlen = 1;
175 msg.msg_control = cbuf.as_mut_ptr() as *mut libc::c_void;
176 msg.msg_controllen = space as _;
177 unsafe {
178 let cmsg = libc::CMSG_FIRSTHDR(&msg);
179 (*cmsg).cmsg_level = libc::SOL_SOCKET;
180 (*cmsg).cmsg_type = libc::SCM_RIGHTS;
181 (*cmsg).cmsg_len = libc::CMSG_LEN(mem::size_of::<RawFd>() as u32) as _;
182 ptr::write_unaligned(libc::CMSG_DATA(cmsg) as *mut RawFd, fd);
183 }
184 loop {
185 let n = unsafe { libc::sendmsg(sock, &msg, libc::MSG_NOSIGNAL) };
186 if n < 0 {
187 let err = io::Error::last_os_error();
188 if err.raw_os_error() == Some(libc::EINTR) {
189 continue;
190 }
191 return Err(err);
192 }
193 if n as usize != config.len() {
194 return Err(io::Error::new(
195 io::ErrorKind::WriteZero,
196 "short config send",
197 ));
198 }
199 return Ok(());
200 }
201}
202
203fn serve(
204 listener: UnixListener,
205 wake: RawFd,
206 config: [u8; CONFIG_SIZE],
207 ring_fd: RawFd,
208 shared: Arc<Shared>,
209) {
210 let listen_fd = listener.as_raw_fd();
211 let mut pollfds: Vec<libc::pollfd> = Vec::new();
212 while !shared.stop.load(Ordering::Acquire) {
213 pollfds.clear();
214 pollfds.push(libc::pollfd {
215 fd: listen_fd,
216 events: libc::POLLIN,
217 revents: 0,
218 });
219 pollfds.push(libc::pollfd {
220 fd: wake,
221 events: libc::POLLIN,
222 revents: 0,
223 });
224 {
225 let clients = shared.clients.lock().unwrap_or_else(|e| e.into_inner());
226 for c in clients.iter() {
227 pollfds.push(libc::pollfd {
228 fd: c.fd.as_raw_fd(),
229 events: libc::POLLIN | libc::POLLRDHUP,
230 revents: 0,
231 });
232 }
233 }
234 let n = unsafe { libc::poll(pollfds.as_mut_ptr(), pollfds.len() as libc::nfds_t, -1) };
235 if n < 0 {
236 if io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
237 continue;
238 }
239 break;
240 }
241 if pollfds[1].revents != 0 {
242 let mut v: u64 = 0;
243 unsafe { libc::read(wake, &mut v as *mut u64 as *mut _, 8) };
244 if shared.stop.load(Ordering::Acquire) {
245 break;
246 }
247 }
248 if pollfds[0].revents != 0 {
249 accept_clients(&listener, &config, ring_fd, &shared);
250 }
251 let mut clients = shared.clients.lock().unwrap_or_else(|e| e.into_inner());
252 let mut idx = 0;
253 for pfd in pollfds.iter().skip(2) {
254 if idx >= clients.len() {
255 break;
256 }
257 if clients[idx].fd.as_raw_fd() != pfd.fd {
258 idx += 1;
259 continue;
260 }
261 if pfd.revents == 0 {
262 idx += 1;
263 continue;
264 }
265 let mut buf = [0u8; 64];
266 let r = unsafe {
267 libc::recv(
268 clients[idx].fd.as_raw_fd(),
269 buf.as_mut_ptr() as *mut _,
270 buf.len(),
271 libc::MSG_DONTWAIT,
272 )
273 };
274 let closed = if r > 0 {
275 if !clients[idx].ready {
276 clients[idx].ready = true;
277 shared.ready_count.fetch_add(1, Ordering::Relaxed);
278 }
279 false
280 } else if r == 0 {
281 true
282 } else {
283 !matches!(
284 io::Error::last_os_error().raw_os_error(),
285 Some(libc::EAGAIN) | Some(libc::EINTR)
286 )
287 };
288 if closed
289 || pfd.revents & (libc::POLLHUP | libc::POLLERR | libc::POLLRDHUP) != 0 && r <= 0
290 {
291 if clients[idx].ready {
292 shared.ready_count.fetch_sub(1, Ordering::Relaxed);
293 }
294 clients.remove(idx);
295 continue;
296 }
297 idx += 1;
298 }
299 }
300}
301
302pub(crate) fn restrict_to_owner(path: &str) -> io::Result<()> {
304 use std::os::unix::fs::PermissionsExt;
305 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
306}
307
308pub(crate) fn peer_is_session_user(fd: RawFd) -> bool {
311 let mut cred: libc::ucred = unsafe { std::mem::zeroed() };
312 let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
313 let r = unsafe {
314 libc::getsockopt(
315 fd,
316 libc::SOL_SOCKET,
317 libc::SO_PEERCRED,
318 (&mut cred as *mut libc::ucred).cast(),
319 &mut len,
320 )
321 };
322 r == 0 && (cred.uid == unsafe { libc::geteuid() } || cred.uid == 0)
323}
324
325fn accept_clients(
326 listener: &UnixListener,
327 config: &[u8; CONFIG_SIZE],
328 ring_fd: RawFd,
329 shared: &Shared,
330) {
331 loop {
332 match listener.accept() {
333 Ok((stream, _)) => {
334 if !peer_is_session_user(stream.as_raw_fd()) {
335 continue;
336 }
337 let _ = stream.set_nonblocking(true);
338 let fd = OwnedFd::from(stream);
339 match send_config_with_fd(fd.as_raw_fd(), config, ring_fd) {
340 Ok(()) => {
341 shared
342 .clients
343 .lock()
344 .unwrap_or_else(|e| e.into_inner())
345 .push(Client { fd, ready: false });
346 }
347 Err(e) => eprintln!("[webcam] interposer handshake failed: {}", e),
348 }
349 }
350 Err(e) if e.kind() == io::ErrorKind::WouldBlock => return,
351 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
352 Err(e) => {
353 eprintln!("[webcam] accept failed: {}", e);
354 return;
355 }
356 }
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use std::io::{Read, Write};
364 use std::os::unix::net::UnixStream;
365 use std::time::{Duration, Instant};
366
367 #[test]
369 fn the_socket_is_connectable_by_its_user_alone() {
370 use std::os::unix::fs::PermissionsExt;
371 let dir = std::env::temp_dir().join(format!("pxwc-mode-{}", std::process::id()));
372 let _ = std::fs::remove_dir_all(&dir);
373 std::fs::create_dir(&dir).unwrap();
374 let path = dir.join("cam.sock").to_string_lossy().into_owned();
375 let old = unsafe { libc::umask(0) };
376 let listener = UnixListener::bind(&path).unwrap();
377 unsafe { libc::umask(old) };
378 restrict_to_owner(&path).unwrap();
379 let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
380 assert_eq!(mode, 0o600);
381 let _client = UnixStream::connect(&path).unwrap();
382 let (peer, _) = listener.accept().unwrap();
383 assert!(peer_is_session_user(peer.as_raw_fd()));
384 std::fs::remove_dir_all(&dir).unwrap();
385 }
386
387 fn recv_config(stream: &UnixStream) -> (Vec<u8>, RawFd) {
388 let mut data = vec![0u8; CONFIG_SIZE];
389 let mut iov = libc::iovec {
390 iov_base: data.as_mut_ptr() as *mut _,
391 iov_len: data.len(),
392 };
393 let space = unsafe { libc::CMSG_SPACE(4) } as usize;
394 let mut cbuf = vec![0u64; space.div_ceil(8)];
395 let mut msg: libc::msghdr = unsafe { mem::zeroed() };
396 msg.msg_iov = &mut iov;
397 msg.msg_iovlen = 1;
398 msg.msg_control = cbuf.as_mut_ptr() as *mut _;
399 msg.msg_controllen = space as _;
400 let n = unsafe { libc::recvmsg(stream.as_raw_fd(), &mut msg, 0) };
401 assert_eq!(n as usize, CONFIG_SIZE);
402 let cmsg = unsafe { libc::CMSG_FIRSTHDR(&msg) };
403 assert!(!cmsg.is_null());
404 let fd = unsafe { ptr::read_unaligned(libc::CMSG_DATA(cmsg) as *const RawFd) };
405 (data, fd)
406 }
407
408 #[test]
409 fn handshake_fd_and_doorbells() {
410 let dir = std::env::temp_dir().join(format!("pxwc-{}", std::process::id()));
411 let path = dir.join("cam.sock");
412 let ring = crate::memfd_create(c"t", libc::MFD_CLOEXEC);
413 assert!(ring >= 0);
414 unsafe { libc::ftruncate(ring, 8192) };
415 let mut config = [0u8; CONFIG_SIZE];
416 config[..4].copy_from_slice(&0xDEADBEEFu32.to_le_bytes());
417 let server = Server::bind(path.to_str().unwrap(), config, ring).unwrap();
418
419 let mut client = UnixStream::connect(&path).unwrap();
420 let (data, fd) = recv_config(&client);
421 assert_eq!(&data[..4], &0xDEADBEEFu32.to_le_bytes());
422 assert!(fd >= 0);
423 let mut st: libc::stat = unsafe { mem::zeroed() };
424 assert_eq!(unsafe { libc::fstat(fd, &mut st) }, 0);
425 assert_eq!(st.st_size, 8192);
426 unsafe { libc::close(fd) };
427
428 server.ring_doorbell();
429 assert_eq!(server.client_count(), 0);
430 client.write_all(&[8u8]).unwrap();
431 let deadline = Instant::now() + Duration::from_secs(3);
432 while server.client_count() != 1 && Instant::now() < deadline {
433 thread::sleep(Duration::from_millis(5));
434 }
435 assert_eq!(server.client_count(), 1);
436 server.ring_doorbell();
437 client
438 .set_read_timeout(Some(Duration::from_secs(2)))
439 .unwrap();
440 let mut b = [0u8; 1];
441 client.read_exact(&mut b).unwrap();
442 assert_eq!(b[0], 1);
443
444 drop(client);
445 let deadline = Instant::now() + Duration::from_secs(3);
446 while server.client_count() != 0 && Instant::now() < deadline {
447 server.ring_doorbell();
448 thread::sleep(Duration::from_millis(5));
449 }
450 assert_eq!(server.client_count(), 0);
451 drop(server);
452 assert!(!path.exists());
453 unsafe { libc::close(ring) };
454 let _ = std::fs::remove_dir_all(&dir);
455 }
456}