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