1use std::os::fd::RawFd;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
11
12use crate::io::ring::{CompletionRing, IoOp};
13use crate::term::Term;
14use crate::term::boxed::{BoxedHeader, BoxedTag};
15
16pub const FD_RESOURCE_PAYLOAD_WORDS: usize = 1;
18pub const FD_RESOURCE_WORDS: usize = 1 + FD_RESOURCE_PAYLOAD_WORDS;
20
21#[derive(Copy, Clone, Debug, Eq, PartialEq)]
23#[repr(u8)]
24pub enum FdState {
25 Open = 0,
26 Closing = 1,
27 Closed = 2,
28}
29
30#[derive(Copy, Clone, Debug, Eq, PartialEq)]
32#[repr(u8)]
33pub enum FdMode {
34 Passive = 0,
35 Active = 1,
36 ActiveOnce = 2,
37}
38
39impl FdMode {
40 fn from_u8(value: u8) -> Self {
41 match value {
42 1 => Self::Active,
43 2 => Self::ActiveOnce,
44 _ => Self::Passive,
45 }
46 }
47}
48
49impl FdState {
50 fn from_u8(value: u8) -> Self {
51 match value {
52 0 => Self::Open,
53 1 => Self::Closing,
54 _ => Self::Closed,
55 }
56 }
57}
58
59#[derive(Debug)]
61pub struct FdInner {
62 fd: RawFd,
63 owner_pid: u64,
64 state: AtomicU8,
65 mode: AtomicU8,
66 controlling_process: AtomicU64,
67 current_offset: AtomicU64,
68}
69
70impl FdInner {
71 pub fn new(fd: RawFd, owner_pid: u64) -> Self {
73 Self {
74 fd,
75 owner_pid,
76 state: AtomicU8::new(FdState::Open as u8),
77 mode: AtomicU8::new(FdMode::Passive as u8),
78 controlling_process: AtomicU64::new(owner_pid),
79 current_offset: AtomicU64::new(0),
80 }
81 }
82
83 #[must_use]
85 pub fn fd(&self) -> RawFd {
86 self.fd
87 }
88
89 #[must_use]
91 pub fn owner_pid(&self) -> u64 {
92 self.owner_pid
93 }
94
95 #[must_use]
97 pub fn state(&self) -> FdState {
98 FdState::from_u8(self.state.load(Ordering::Acquire))
99 }
100
101 #[must_use]
103 pub fn mode(&self) -> FdMode {
104 FdMode::from_u8(self.mode.load(Ordering::Acquire))
105 }
106
107 pub fn set_mode(&self, mode: FdMode) {
109 self.mode.store(mode as u8, Ordering::Release);
110 }
111
112 #[must_use]
114 pub fn controlling_process(&self) -> u64 {
115 self.controlling_process.load(Ordering::Acquire)
116 }
117
118 pub fn set_controlling_process(&self, pid: u64) {
120 self.controlling_process.store(pid, Ordering::Release);
121 }
122
123 #[must_use]
125 pub fn current_offset(&self) -> u64 {
126 self.current_offset.load(Ordering::Acquire)
127 }
128
129 pub fn set_current_offset(&self, offset: u64) {
131 self.current_offset.store(offset, Ordering::Release);
132 }
133
134 pub fn advance_current_offset(&self, bytes: usize) {
136 let delta = u64::try_from(bytes).unwrap_or(u64::MAX);
137 let _previous =
138 self.current_offset
139 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
140 Some(current.saturating_add(delta))
141 });
142 }
143
144 pub fn explicit_close(&self, ring: &dyn CompletionRing) -> bool {
147 self.explicit_close_with_op_id(ring).is_some()
148 }
149
150 pub fn explicit_close_with_op_id(&self, ring: &dyn CompletionRing) -> Option<u64> {
152 if self.begin_close() {
153 Some(ring.submit(IoOp::Close { fd: self.fd }))
154 } else {
155 None
156 }
157 }
158
159 pub fn mark_closed(&self) {
161 self.state.store(FdState::Closed as u8, Ordering::Release);
162 }
163
164 pub fn close_synchronously(&self) -> bool {
166 if self.begin_close() {
167 self.close_fd_synchronously();
168 true
169 } else {
170 false
171 }
172 }
173
174 fn begin_close(&self) -> bool {
175 self.fd >= 0
176 && self
177 .state
178 .compare_exchange(
179 FdState::Open as u8,
180 FdState::Closing as u8,
181 Ordering::AcqRel,
182 Ordering::Acquire,
183 )
184 .is_ok()
185 }
186
187 fn close_fd_synchronously(&self) {
188 let _result = unsafe { libc::close(self.fd) };
191 self.state.store(FdState::Closed as u8, Ordering::Release);
192 }
193}
194
195impl Drop for FdInner {
196 fn drop(&mut self) {
197 if self.begin_close() {
198 self.close_fd_synchronously();
199 }
200 }
201}
202
203#[derive(Copy, Clone, Debug)]
205pub struct FdResource {
206 ptr: *const u64,
207}
208
209impl FdResource {
210 pub fn new(term: Term) -> Option<Self> {
212 if !term.is_boxed() {
213 return None;
214 }
215 let ptr = term.heap_ptr()?;
216 let header = unsafe { *ptr };
218 if BoxedHeader::tag(header) != Some(BoxedTag::FdResource) {
219 return None;
220 }
221 if BoxedHeader::size(header) != FD_RESOURCE_PAYLOAD_WORDS {
222 return None;
223 }
224 if unsafe { *ptr.add(1) } == 0 {
227 return None;
228 }
229 Some(Self { ptr })
230 }
231
232 #[must_use]
234 pub fn fd(self) -> RawFd {
235 self.inner_ref().fd()
236 }
237
238 #[must_use]
240 pub fn mode(self) -> FdMode {
241 self.inner_ref().mode()
242 }
243
244 #[must_use]
246 pub fn owner_pid(self) -> u64 {
247 self.inner_ref().owner_pid()
248 }
249
250 #[must_use]
252 pub fn state(self) -> FdState {
253 self.inner_ref().state()
254 }
255
256 #[must_use]
258 pub fn controlling_process(self) -> u64 {
259 self.inner_ref().controlling_process()
260 }
261
262 pub fn set_mode(self, mode: FdMode) {
264 self.inner_ref().set_mode(mode);
265 }
266
267 pub fn set_controlling_process(self, pid: u64) {
269 self.inner_ref().set_controlling_process(pid);
270 }
271
272 #[must_use]
274 pub fn inner(self) -> Arc<FdInner> {
275 clone_fd_inner_from_raw_word(self.arc_ptr_word())
276 }
277
278 fn inner_ref(self) -> &'static FdInner {
279 let ptr = self.arc_ptr_word() as *const FdInner;
280 unsafe { &*ptr }
283 }
284
285 fn arc_ptr_word(self) -> u64 {
286 unsafe { *self.ptr.add(1) }
288 }
289}
290
291pub fn write_fd_resource(heap: &mut [u64], fd_inner: Arc<FdInner>) -> Option<Term> {
293 if heap.len() < FD_RESOURCE_WORDS {
294 return None;
295 }
296
297 heap[0] = BoxedHeader::new(BoxedTag::FdResource, FD_RESOURCE_PAYLOAD_WORDS);
298 heap[1] = Arc::into_raw(fd_inner) as u64;
299
300 Some(Term::boxed_ptr(heap.as_ptr()))
301}
302
303pub(crate) fn clone_fd_inner_from_raw_word(raw: u64) -> Arc<FdInner> {
304 let ptr = raw as *const FdInner;
305 let arc = unsafe { Arc::from_raw(ptr) };
310 let cloned = Arc::clone(&arc);
311 let _raw = Arc::into_raw(arc);
312 cloned
313}
314
315pub(crate) fn retain_fd_inner_arc(ptr: *const u64) {
316 let raw = read_raw_word(ptr, 1);
317 let arc_ptr = raw as *const FdInner;
318 let source = unsafe { Arc::from_raw(arc_ptr) };
323 let copied = Arc::clone(&source);
324 let _source_raw = Arc::into_raw(source);
325 let copied_raw = Arc::into_raw(copied);
326 write_raw_word(ptr, 1, copied_raw as u64);
327}
328
329pub(crate) fn release_fd_inner_arc(ptr: *const u64) {
330 let raw = read_raw_word(ptr, 1);
331 if raw == 0 {
332 return;
333 }
334 let arc_ptr = raw as *const FdInner;
335 let _source = unsafe { Arc::from_raw(arc_ptr) };
339 write_raw_word(ptr, 1, 0);
340}
341
342pub(crate) fn close_owned_resource_at(ptr: *const u64, owner_pid: u64) {
343 let raw = read_raw_word(ptr, 1);
344 if raw == 0 {
345 return;
346 }
347 let inner = clone_fd_inner_from_raw_word(raw);
348 if inner.owner_pid() == owner_pid {
349 let _closed = inner.close_synchronously();
350 }
351}
352
353fn read_raw_word(ptr: *const u64, offset: usize) -> u64 {
354 unsafe { *ptr.add(offset) }
357}
358
359fn write_raw_word(ptr: *const u64, offset: usize, value: u64) {
360 unsafe { *(ptr as *mut u64).add(offset) = value }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use std::sync::Mutex;
369
370 use crate::io::ring::IoCompletion;
371
372 #[derive(Default)]
373 struct MockRing {
374 submitted: Mutex<Vec<IoOp>>,
375 }
376
377 impl CompletionRing for MockRing {
378 fn submit(&self, op: IoOp) -> u64 {
379 if let Ok(mut submitted) = self.submitted.lock() {
380 submitted.push(op);
381 submitted.len() as u64
382 } else {
383 0
384 }
385 }
386
387 fn poll_completions(&self, _timeout: std::time::Duration) -> Vec<IoCompletion> {
388 Vec::new()
389 }
390
391 fn pending_count(&self) -> usize {
392 self.submitted.lock().map(|ops| ops.len()).unwrap_or(0)
393 }
394
395 fn shutdown(&self) {}
396 }
397
398 fn pipe_read_fd() -> RawFd {
399 let mut fds = [0; 2];
400 let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
402 assert_eq!(rc, 0);
403 let _closed = unsafe { libc::close(fds[1]) };
405 fds[0]
406 }
407
408 fn fd_identity(fd: RawFd) -> (libc::dev_t, libc::ino_t) {
412 let mut st: libc::stat = unsafe { std::mem::zeroed() };
415 let rc = unsafe { libc::fstat(fd, &mut st) };
416 assert_eq!(rc, 0, "expected an open fd to fstat successfully");
417 (st.st_dev, st.st_ino)
418 }
419
420 fn fd_closed_since(fd: RawFd, original: (libc::dev_t, libc::ino_t)) -> bool {
429 let mut st: libc::stat = unsafe { std::mem::zeroed() };
431 let rc = unsafe { libc::fstat(fd, &mut st) };
432 if rc != 0 {
433 return true;
434 }
435 (st.st_dev, st.st_ino) != original
436 }
437
438 #[test]
439 fn write_and_access_fd_resource_round_trips_fd_and_owner() {
440 let fd = pipe_read_fd();
441 let fd_id = fd_identity(fd);
442 let inner = Arc::new(FdInner::new(fd, 42));
443 let retained = Arc::clone(&inner);
444 let mut heap = [0_u64; FD_RESOURCE_WORDS];
445 let term = write_fd_resource(&mut heap, retained).expect("fd resource should fit");
446
447 assert_eq!(BoxedHeader::tag(heap[0]), Some(BoxedTag::FdResource));
448 assert_eq!(BoxedHeader::size(heap[0]), FD_RESOURCE_PAYLOAD_WORDS);
449 assert_ne!(heap[1], 0);
450 let resource = FdResource::new(term).expect("valid fd resource");
451 assert_eq!(resource.fd(), fd);
452 assert_eq!(resource.owner_pid(), 42);
453 assert_eq!(resource.state(), FdState::Open);
454 assert_eq!(resource.inner().current_offset(), 0);
455 assert_eq!(Arc::strong_count(&inner), 2);
456
457 release_fd_inner_arc(heap.as_ptr());
458 drop(inner);
459 assert!(fd_closed_since(fd, fd_id));
460 }
461
462 #[test]
463 fn write_fd_resource_rejects_too_small_heap_slice() {
464 let fd = pipe_read_fd();
465 let fd_id = fd_identity(fd);
466 let inner = Arc::new(FdInner::new(fd, 7));
467 let mut heap = [0_u64; 1];
468 assert!(write_fd_resource(&mut heap, inner).is_none());
469 assert!(fd_closed_since(fd, fd_id));
470 }
471
472 #[test]
473 fn fd_resource_accessor_rejects_invalid_tag_size_and_null_pointer() {
474 let mut heap = [0_u64; FD_RESOURCE_WORDS];
475 heap[0] = BoxedHeader::new(BoxedTag::Tuple, FD_RESOURCE_PAYLOAD_WORDS);
476 heap[1] = 1;
477 assert!(FdResource::new(Term::boxed_ptr(heap.as_ptr())).is_none());
478
479 heap[0] = BoxedHeader::new(BoxedTag::FdResource, 2);
480 assert!(FdResource::new(Term::boxed_ptr(heap.as_ptr())).is_none());
481
482 heap[0] = BoxedHeader::new(BoxedTag::FdResource, FD_RESOURCE_PAYLOAD_WORDS);
483 heap[1] = 0;
484 assert!(FdResource::new(Term::boxed_ptr(heap.as_ptr())).is_none());
485 }
486
487 #[test]
488 fn last_arc_drop_closes_fd() {
489 let fd = pipe_read_fd();
490 let fd_id = fd_identity(fd);
491 let inner = Arc::new(FdInner::new(fd, 1));
492 drop(inner);
493 assert!(fd_closed_since(fd, fd_id));
494 }
495
496 #[test]
497 fn explicit_close_then_drop_does_not_submit_or_close_twice() {
498 let fd = pipe_read_fd();
499 let inner = Arc::new(FdInner::new(fd, 1));
500 let ring = MockRing::default();
501
502 assert!(inner.explicit_close(&ring));
503 assert!(!inner.explicit_close(&ring));
504 assert_eq!(inner.state(), FdState::Closing);
505 let submitted_len = ring.submitted.lock().map(|ops| ops.len()).unwrap_or(0);
506 assert_eq!(submitted_len, 1);
507 drop(inner);
508
509 let _closed = unsafe { libc::close(fd) };
514 }
515}