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 _ = unsafe { &mut (*storage).reserved };
132            reserved.cast::<T>()
133        }
134
135        unsafe extern "C" fn destroy(io: *mut zxio_sys::zxio_t) {
136            unsafe { 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 = unsafe { &*Self::to_data(io) };
147            match data.writev(unsafe {
148                std::slice::from_raw_parts(vector.cast::<zx::sys::zx_iovec_t>(), vector_count)
149            }) {
150                Ok(count) => {
151                    unsafe { *out_actual = count };
152                    zx::sys::ZX_OK
153                }
154                Err(status) => status.into_raw(),
155            }
156        }
157
158        unsafe extern "C" fn isatty(
159            io: *mut zxio_sys::zxio_t,
160            tty: *mut bool,
161        ) -> zxio_sys::zx_status_t {
162            let data = unsafe { &*Self::to_data(io) };
163            match data.isatty() {
164                Ok(result) => {
165                    unsafe { *tty = result };
166                    zx::sys::ZX_OK
167                }
168                Err(status) => status.into_raw(),
169            }
170        }
171
172        const OPS: zxio_sys::zxio_ops = zxio_sys::zxio_ops {
173            destroy: Some(Self::destroy),
174            writev: Some(Self::writev),
175            isatty: Some(Self::isatty),
176            ..DEFAULT_ZXIO_OPS
177        };
178    }
179
180    unsafe {
181        zxio_sys::zxio_init(storage.cast::<zxio_sys::zxio_tag>(), &Adapter::<T>::OPS);
182    }
183
184    // The fdio object is always consumed.
185    let bound_fd = unsafe { fdio_sys::fdio_bind_to_fd(fdio, fd, 0) };
186    if bound_fd < 0 {
187        return Err(zx::Status::BAD_STATE);
188    }
189    // We requested a specific fd, we expect to have gotten it, or failed.
190    assert_eq!(bound_fd, fd);
191    Ok(())
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use std::sync::Arc;
198    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
199
200    struct MockFdOps {
201        dropped_counter: Arc<AtomicUsize>,
202        writev_cb: Option<Box<dyn Fn(&[zx::sys::zx_iovec_t]) + Send + 'static>>,
203        isatty_cb: Option<Box<dyn Fn() -> bool + Send + 'static>>,
204    }
205
206    impl Drop for MockFdOps {
207        fn drop(&mut self) {
208            self.dropped_counter.fetch_add(1, Ordering::Relaxed);
209        }
210    }
211
212    impl FdOps for MockFdOps {
213        fn writev(&self, iovecs: &[zx::sys::zx_iovec_t]) -> Result<usize, zx::Status> {
214            if let Some(cb) = self.writev_cb.as_ref() {
215                cb(iovecs);
216            }
217            Ok(iovecs.iter().map(|v| v.capacity).sum())
218        }
219
220        fn isatty(&self) -> Result<bool, zx::Status> {
221            if let Some(cb) = self.isatty_cb.as_ref() { Ok(cb()) } else { Ok(false) }
222        }
223    }
224
225    #[fuchsia::test]
226    fn test_bind_to_fd_with_ops() {
227        let writev_called = Arc::new(AtomicBool::new(false));
228        let isatty_called = Arc::new(AtomicBool::new(false));
229        let dropped_counter = Arc::new(AtomicUsize::new(0));
230
231        {
232            let writev_called = writev_called.clone();
233            let isatty_called = isatty_called.clone();
234            let ops = MockFdOps {
235                dropped_counter: dropped_counter.clone(),
236                writev_cb: Some(Box::new(move |iovecs| {
237                    writev_called.store(true, Ordering::Relaxed);
238                    assert_eq!(iovecs.len(), 1);
239                    let written_data = unsafe {
240                        std::slice::from_raw_parts(
241                            iovecs[0].buffer as *const u8,
242                            iovecs[0].capacity,
243                        )
244                    };
245                    assert_eq!(written_data, b"hello\n");
246                })),
247                isatty_cb: Some(Box::new(move || {
248                    isatty_called.store(true, Ordering::Relaxed);
249                    true
250                })),
251            };
252
253            // Bind stdout.
254            bind_to_fd_with_ops(ops, 1).unwrap();
255        }
256
257        assert!(!writev_called.load(Ordering::Relaxed));
258        assert!(!isatty_called.load(Ordering::Relaxed));
259        println!("hello");
260        assert!(writev_called.load(Ordering::Relaxed));
261
262        assert_eq!(unsafe { libc::isatty(1) }, 1);
263        assert!(isatty_called.load(Ordering::Relaxed));
264
265        assert_eq!(dropped_counter.load(Ordering::Relaxed), 0);
266
267        // Binding another set of mock operations should cause the previous ones
268        // to be dropped.
269        bind_to_fd_with_ops(
270            MockFdOps {
271                dropped_counter: dropped_counter.clone(),
272                writev_cb: None,
273                isatty_cb: None,
274            },
275            1,
276        )
277        .unwrap();
278
279        assert_eq!(dropped_counter.load(Ordering::Relaxed), 1);
280    }
281
282    #[fuchsia::test]
283    fn test_bind_failure() {
284        let dropped_counter = Arc::new(AtomicUsize::new(0));
285        assert_eq!(
286            bind_to_fd_with_ops(
287                MockFdOps {
288                    dropped_counter: dropped_counter.clone(),
289                    writev_cb: None,
290                    isatty_cb: None
291                },
292                -1
293            ),
294            Err(zx::Status::INVALID_ARGS)
295        );
296        assert_eq!(dropped_counter.load(Ordering::Relaxed), 1);
297
298        assert_eq!(
299            bind_to_fd_with_ops(
300                MockFdOps {
301                    dropped_counter: dropped_counter.clone(),
302                    writev_cb: None,
303                    isatty_cb: None
304                },
305                1234
306            ),
307            Err(zx::Status::BAD_STATE)
308        );
309        assert_eq!(dropped_counter.load(Ordering::Relaxed), 2);
310    }
311}