Skip to main content

fdio_zxio/
fdio_zxio.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5mod fdio_sys;
6mod zxio_sys;
7
8use std::marker::PhantomData;
9use std::os::fd::RawFd;
10
11/// Custom operations for an fd.
12pub trait FdOps: Sized + Send + 'static {
13    fn writev(&self, _iovecs: &[zx::sys::zx_iovec_t]) -> Result<usize, zx::Status> {
14        Err(zx::Status::WRONG_TYPE)
15    }
16
17    fn isatty(&self) -> Result<bool, zx::Status> {
18        Ok(false)
19    }
20
21    // NOTE: Support for more operations can be added as and when required.
22}
23
24/// A copy of `zxio_default_ops`.
25const DEFAULT_ZXIO_OPS: zxio_sys::zxio_ops = zxio_sys::zxio_ops {
26    destroy: Some(zxio_sys::zxio_default_destroy),
27    close: Some(zxio_sys::zxio_default_close),
28    release: Some(zxio_sys::zxio_default_release),
29    borrow: Some(zxio_sys::zxio_default_borrow),
30    clone: Some(zxio_sys::zxio_default_clone),
31    wait_begin: Some(zxio_sys::zxio_default_wait_begin),
32    wait_end: Some(zxio_sys::zxio_default_wait_end),
33    sync: Some(zxio_sys::zxio_default_sync),
34    attr_get: Some(zxio_sys::zxio_default_attr_get),
35    attr_set: Some(zxio_sys::zxio_default_attr_set),
36    readv: Some(zxio_sys::zxio_default_readv),
37    readv_at: Some(zxio_sys::zxio_default_readv_at),
38    writev: Some(zxio_sys::zxio_default_writev),
39    writev_at: Some(zxio_sys::zxio_default_writev_at),
40    seek: Some(zxio_sys::zxio_default_seek),
41    truncate: Some(zxio_sys::zxio_default_truncate),
42    flags_get: Some(zxio_sys::zxio_default_flags_get),
43    flags_set: Some(zxio_sys::zxio_default_flags_set),
44    vmo_get: Some(zxio_sys::zxio_default_vmo_get),
45    on_mapped: Some(zxio_sys::zxio_default_on_mapped),
46    get_read_buffer_available: Some(zxio_sys::zxio_default_get_read_buffer_available),
47    shutdown: Some(zxio_sys::zxio_default_shutdown),
48    unlink: Some(zxio_sys::zxio_default_unlink),
49    token_get: Some(zxio_sys::zxio_default_token_get),
50    rename: Some(zxio_sys::zxio_default_rename),
51    link: Some(zxio_sys::zxio_default_link),
52    link_into: Some(zxio_sys::zxio_default_link_into),
53    dirent_iterator_init: Some(zxio_sys::zxio_default_dirent_iterator_init),
54    dirent_iterator_next: Some(zxio_sys::zxio_default_dirent_iterator_next),
55    dirent_iterator_rewind: Some(zxio_sys::zxio_default_dirent_iterator_rewind),
56    dirent_iterator_destroy: Some(zxio_sys::zxio_default_dirent_iterator_destroy),
57    isatty: Some(zxio_sys::zxio_default_isatty),
58    get_window_size: Some(zxio_sys::zxio_default_get_window_size),
59    set_window_size: Some(zxio_sys::zxio_default_set_window_size),
60    advisory_lock: Some(zxio_sys::zxio_default_advisory_lock),
61    watch_directory: Some(zxio_sys::zxio_default_watch_directory),
62    bind: Some(zxio_sys::zxio_default_bind),
63    connect: Some(zxio_sys::zxio_default_connect),
64    listen: Some(zxio_sys::zxio_default_listen),
65    accept: Some(zxio_sys::zxio_default_accept),
66    getsockname: Some(zxio_sys::zxio_default_getsockname),
67    getpeername: Some(zxio_sys::zxio_default_getpeername),
68    getsockopt: Some(zxio_sys::zxio_default_getsockopt),
69    setsockopt: Some(zxio_sys::zxio_default_setsockopt),
70    recvmsg: Some(zxio_sys::zxio_default_recvmsg),
71    sendmsg: Some(zxio_sys::zxio_default_sendmsg),
72    ioctl: Some(zxio_sys::zxio_default_ioctl),
73    read_link: Some(zxio_sys::zxio_default_read_link),
74    create_symlink: Some(zxio_sys::zxio_default_create_symlink),
75    xattr_list: Some(zxio_sys::zxio_default_xattr_list),
76    xattr_get: Some(zxio_sys::zxio_default_xattr_get),
77    xattr_set: Some(zxio_sys::zxio_default_xattr_set),
78    xattr_remove: Some(zxio_sys::zxio_default_xattr_remove),
79    open: Some(zxio_sys::zxio_default_open),
80    allocate: Some(zxio_sys::zxio_default_allocate),
81    enable_verity: Some(zxio_sys::zxio_default_enable_verity),
82    set_token_resolver: Some(zxio_sys::zxio_default_set_token_resolver),
83};
84
85/// Bind `ops` to a specific file descriptor.
86///
87/// NOTE: Due to the underlying use of fdio, error cases might also clobber errno.
88pub fn bind_to_fd_with_ops<T: FdOps>(ops: T, fd: RawFd) -> Result<(), zx::Status> {
89    struct AssertCompatible<T>(PhantomData<T>);
90
91    impl<T> AssertCompatible<T> {
92        const SIZE_OK: () = assert!(
93            std::mem::size_of::<T>() <= std::mem::size_of::<zxio_sys::zxio_private>(),
94            "bad size"
95        );
96        const ALIGNMENT_OK: () = assert!(
97            std::mem::align_of::<T>() <= std::mem::align_of::<zxio_sys::zxio_private>(),
98            "bad alignment"
99        );
100    }
101
102    let () = AssertCompatible::<T>::SIZE_OK;
103    let () = AssertCompatible::<T>::ALIGNMENT_OK;
104
105    if fd < 0 {
106        // fdio_bind_to_fd supports finding the next available fd when provided with a negative
107        // number, but due to lack of use-cases for this in Rust this is currently unsupported by
108        // this function.
109        return Err(zx::Status::INVALID_ARGS);
110    }
111
112    let mut storage = std::ptr::null_mut();
113    let fdio = unsafe { fdio_sys::fdio_zxio_create(&mut storage) };
114
115    if fdio.is_null() {
116        return Err(zx::Status::INTERNAL);
117    }
118
119    // NOTE: We now own `fdio`, so we must take care not to leak.
120
121    unsafe {
122        let reserved: *mut _ = &mut (*storage.cast::<zxio_sys::zxio_storage>()).reserved;
123        reserved.cast::<T>().write(ops);
124    }
125
126    struct Adapter<T>(PhantomData<T>);
127    impl<T: FdOps> Adapter<T> {
128        unsafe fn to_data(zxio: *mut zxio_sys::zxio_t) -> *mut T {
129            let storage = zxio.cast::<zxio_sys::zxio_storage>();
130            let reserved: *mut _ = unsafe { &mut (*storage).reserved };
131            reserved.cast::<T>()
132        }
133
134        unsafe extern "C" fn destroy(io: *mut zxio_sys::zxio_t) {
135            unsafe { std::ptr::drop_in_place(Self::to_data(io)) };
136        }
137
138        unsafe extern "C" fn writev(
139            io: *mut zxio_sys::zxio_t,
140            vector: *const zxio_sys::zx_iovec_t,
141            vector_count: usize,
142            _flags: zxio_sys::zxio_flags_t,
143            out_actual: *mut usize,
144        ) -> zxio_sys::zx_status_t {
145            let data = unsafe { &*Self::to_data(io) };
146            match data.writev(unsafe {
147                std::slice::from_raw_parts(vector.cast::<zx::sys::zx_iovec_t>(), vector_count)
148            }) {
149                Ok(count) => {
150                    unsafe { *out_actual = count };
151                    zx::sys::ZX_OK
152                }
153                Err(status) => status.into_raw(),
154            }
155        }
156
157        unsafe extern "C" fn isatty(
158            io: *mut zxio_sys::zxio_t,
159            tty: *mut bool,
160        ) -> zxio_sys::zx_status_t {
161            let data = unsafe { &*Self::to_data(io) };
162            match data.isatty() {
163                Ok(result) => {
164                    unsafe { *tty = result };
165                    zx::sys::ZX_OK
166                }
167                Err(status) => status.into_raw(),
168            }
169        }
170
171        const OPS: zxio_sys::zxio_ops = zxio_sys::zxio_ops {
172            destroy: Some(Self::destroy),
173            writev: Some(Self::writev),
174            isatty: Some(Self::isatty),
175            ..DEFAULT_ZXIO_OPS
176        };
177    }
178
179    unsafe {
180        zxio_sys::zxio_init(storage.cast::<zxio_sys::zxio_tag>(), &Adapter::<T>::OPS);
181    }
182
183    // The fdio object is always consumed.
184    let bound_fd = unsafe { fdio_sys::fdio_bind_to_fd(fdio, fd, 0) };
185    if bound_fd < 0 {
186        return Err(zx::Status::BAD_STATE);
187    }
188    // We requested a specific fd, we expect to have gotten it, or failed.
189    assert_eq!(bound_fd, fd);
190    Ok(())
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use std::sync::Arc;
197    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
198
199    struct MockFdOps {
200        dropped_counter: Arc<AtomicUsize>,
201        writev_cb: Option<Box<dyn Fn(&[zx::sys::zx_iovec_t]) + Send + 'static>>,
202        isatty_cb: Option<Box<dyn Fn() -> bool + Send + 'static>>,
203    }
204
205    impl Drop for MockFdOps {
206        fn drop(&mut self) {
207            self.dropped_counter.fetch_add(1, Ordering::Relaxed);
208        }
209    }
210
211    impl FdOps for MockFdOps {
212        fn writev(&self, iovecs: &[zx::sys::zx_iovec_t]) -> Result<usize, zx::Status> {
213            if let Some(cb) = self.writev_cb.as_ref() {
214                cb(iovecs);
215            }
216            Ok(iovecs.iter().map(|v| v.capacity).sum())
217        }
218
219        fn isatty(&self) -> Result<bool, zx::Status> {
220            if let Some(cb) = self.isatty_cb.as_ref() { Ok(cb()) } else { Ok(false) }
221        }
222    }
223
224    #[fuchsia::test]
225    fn test_bind_to_fd_with_ops() {
226        let writev_called = Arc::new(AtomicBool::new(false));
227        let isatty_called = Arc::new(AtomicBool::new(false));
228        let dropped_counter = Arc::new(AtomicUsize::new(0));
229
230        {
231            let writev_called = writev_called.clone();
232            let isatty_called = isatty_called.clone();
233            let ops = MockFdOps {
234                dropped_counter: dropped_counter.clone(),
235                writev_cb: Some(Box::new(move |iovecs| {
236                    writev_called.store(true, Ordering::Relaxed);
237                    assert_eq!(iovecs.len(), 1);
238                    let written_data = unsafe {
239                        std::slice::from_raw_parts(
240                            iovecs[0].buffer as *const u8,
241                            iovecs[0].capacity,
242                        )
243                    };
244                    assert_eq!(written_data, b"hello\n");
245                })),
246                isatty_cb: Some(Box::new(move || {
247                    isatty_called.store(true, Ordering::Relaxed);
248                    true
249                })),
250            };
251
252            // Bind stdout.
253            bind_to_fd_with_ops(ops, 1).unwrap();
254        }
255
256        assert!(!writev_called.load(Ordering::Relaxed));
257        assert!(!isatty_called.load(Ordering::Relaxed));
258        println!("hello");
259        assert!(writev_called.load(Ordering::Relaxed));
260
261        assert_eq!(unsafe { libc::isatty(1) }, 1);
262        assert!(isatty_called.load(Ordering::Relaxed));
263
264        assert_eq!(dropped_counter.load(Ordering::Relaxed), 0);
265
266        // Binding another set of mock operations should cause the previous ones
267        // to be dropped.
268        bind_to_fd_with_ops(
269            MockFdOps {
270                dropped_counter: dropped_counter.clone(),
271                writev_cb: None,
272                isatty_cb: None,
273            },
274            1,
275        )
276        .unwrap();
277
278        assert_eq!(dropped_counter.load(Ordering::Relaxed), 1);
279    }
280
281    #[fuchsia::test]
282    fn test_bind_failure() {
283        let dropped_counter = Arc::new(AtomicUsize::new(0));
284        assert_eq!(
285            bind_to_fd_with_ops(
286                MockFdOps {
287                    dropped_counter: dropped_counter.clone(),
288                    writev_cb: None,
289                    isatty_cb: None
290                },
291                -1
292            ),
293            Err(zx::Status::INVALID_ARGS)
294        );
295        assert_eq!(dropped_counter.load(Ordering::Relaxed), 1);
296
297        assert_eq!(
298            bind_to_fd_with_ops(
299                MockFdOps {
300                    dropped_counter: dropped_counter.clone(),
301                    writev_cb: None,
302                    isatty_cb: None
303                },
304                1234
305            ),
306            Err(zx::Status::BAD_STATE)
307        );
308        assert_eq!(dropped_counter.load(Ordering::Relaxed), 2);
309    }
310}