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};
83
84/// Bind `ops` to a specific file descriptor.
85///
86/// NOTE: Due to the underlying use of fdio, error cases might also clobber errno.
87pub fn bind_to_fd_with_ops<T: FdOps>(ops: T, fd: RawFd) -> Result<(), zx::Status> {
88    struct AssertCompatible<T>(PhantomData<T>);
89
90    impl<T> AssertCompatible<T> {
91        const SIZE_OK: () = assert!(
92            std::mem::size_of::<T>() <= std::mem::size_of::<zxio_sys::zxio_private>(),
93            "bad size"
94        );
95        const ALIGNMENT_OK: () = assert!(
96            std::mem::align_of::<T>() <= std::mem::align_of::<zxio_sys::zxio_private>(),
97            "bad alignment"
98        );
99    }
100
101    let () = AssertCompatible::<T>::SIZE_OK;
102    let () = AssertCompatible::<T>::ALIGNMENT_OK;
103
104    if fd < 0 {
105        // fdio_bind_to_fd supports finding the next available fd when provided with a negative
106        // number, but due to lack of use-cases for this in Rust this is currently unsupported by
107        // this function.
108        return Err(zx::Status::INVALID_ARGS);
109    }
110
111    let mut storage = std::ptr::null_mut();
112    let fdio = unsafe { fdio_sys::fdio_zxio_create(&mut storage) };
113
114    if fdio.is_null() {
115        return Err(zx::Status::INTERNAL);
116    }
117
118    // NOTE: We now own `fdio`, so we must take care not to leak.
119
120    unsafe {
121        let reserved: *mut _ = &mut (*storage.cast::<zxio_sys::zxio_storage>()).reserved;
122        reserved.cast::<T>().write(ops);
123    }
124
125    struct Adapter<T>(PhantomData<T>);
126    impl<T: FdOps> Adapter<T> {
127        unsafe fn to_data(zxio: *mut zxio_sys::zxio_t) -> *mut T {
128            let storage = zxio.cast::<zxio_sys::zxio_storage>();
129            let reserved: *mut _ = unsafe { &mut (*storage).reserved };
130            reserved.cast::<T>()
131        }
132
133        unsafe extern "C" fn destroy(io: *mut zxio_sys::zxio_t) {
134            unsafe { std::ptr::drop_in_place(Self::to_data(io)) };
135        }
136
137        unsafe extern "C" fn writev(
138            io: *mut zxio_sys::zxio_t,
139            vector: *const zxio_sys::zx_iovec_t,
140            vector_count: usize,
141            _flags: zxio_sys::zxio_flags_t,
142            out_actual: *mut usize,
143        ) -> zxio_sys::zx_status_t {
144            let data = unsafe { &*Self::to_data(io) };
145            match data.writev(unsafe {
146                std::slice::from_raw_parts(vector.cast::<zx::sys::zx_iovec_t>(), vector_count)
147            }) {
148                Ok(count) => {
149                    unsafe { *out_actual = count };
150                    zx::sys::ZX_OK
151                }
152                Err(status) => status.into_raw(),
153            }
154        }
155
156        unsafe extern "C" fn isatty(
157            io: *mut zxio_sys::zxio_t,
158            tty: *mut bool,
159        ) -> zxio_sys::zx_status_t {
160            let data = unsafe { &*Self::to_data(io) };
161            match data.isatty() {
162                Ok(result) => {
163                    unsafe { *tty = result };
164                    zx::sys::ZX_OK
165                }
166                Err(status) => status.into_raw(),
167            }
168        }
169
170        const OPS: zxio_sys::zxio_ops = zxio_sys::zxio_ops {
171            destroy: Some(Self::destroy),
172            writev: Some(Self::writev),
173            isatty: Some(Self::isatty),
174            ..DEFAULT_ZXIO_OPS
175        };
176    }
177
178    unsafe {
179        zxio_sys::zxio_init(storage.cast::<zxio_sys::zxio_tag>(), &Adapter::<T>::OPS);
180    }
181
182    // The fdio object is always consumed.
183    let bound_fd = unsafe { fdio_sys::fdio_bind_to_fd(fdio, fd, 0) };
184    if bound_fd < 0 {
185        return Err(zx::Status::BAD_STATE);
186    }
187    // We requested a specific fd, we expect to have gotten it, or failed.
188    assert_eq!(bound_fd, fd);
189    Ok(())
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use std::sync::Arc;
196    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
197
198    struct MockFdOps {
199        dropped_counter: Arc<AtomicUsize>,
200        writev_cb: Option<Box<dyn Fn(&[zx::sys::zx_iovec_t]) + Send + 'static>>,
201        isatty_cb: Option<Box<dyn Fn() -> bool + Send + 'static>>,
202    }
203
204    impl Drop for MockFdOps {
205        fn drop(&mut self) {
206            self.dropped_counter.fetch_add(1, Ordering::Relaxed);
207        }
208    }
209
210    impl FdOps for MockFdOps {
211        fn writev(&self, iovecs: &[zx::sys::zx_iovec_t]) -> Result<usize, zx::Status> {
212            if let Some(cb) = self.writev_cb.as_ref() {
213                cb(iovecs);
214            }
215            Ok(iovecs.iter().map(|v| v.capacity).sum())
216        }
217
218        fn isatty(&self) -> Result<bool, zx::Status> {
219            if let Some(cb) = self.isatty_cb.as_ref() { Ok(cb()) } else { Ok(false) }
220        }
221    }
222
223    #[fuchsia::test]
224    fn test_bind_to_fd_with_ops() {
225        let writev_called = Arc::new(AtomicBool::new(false));
226        let isatty_called = Arc::new(AtomicBool::new(false));
227        let dropped_counter = Arc::new(AtomicUsize::new(0));
228
229        {
230            let writev_called = writev_called.clone();
231            let isatty_called = isatty_called.clone();
232            let ops = MockFdOps {
233                dropped_counter: dropped_counter.clone(),
234                writev_cb: Some(Box::new(move |iovecs| {
235                    writev_called.store(true, Ordering::Relaxed);
236                    assert_eq!(iovecs.len(), 1);
237                    let written_data = unsafe {
238                        std::slice::from_raw_parts(
239                            iovecs[0].buffer as *const u8,
240                            iovecs[0].capacity,
241                        )
242                    };
243                    assert_eq!(written_data, b"hello\n");
244                })),
245                isatty_cb: Some(Box::new(move || {
246                    isatty_called.store(true, Ordering::Relaxed);
247                    true
248                })),
249            };
250
251            // Bind stdout.
252            bind_to_fd_with_ops(ops, 1).unwrap();
253        }
254
255        assert!(!writev_called.load(Ordering::Relaxed));
256        assert!(!isatty_called.load(Ordering::Relaxed));
257        println!("hello");
258        assert!(writev_called.load(Ordering::Relaxed));
259
260        assert_eq!(unsafe { libc::isatty(1) }, 1);
261        assert!(isatty_called.load(Ordering::Relaxed));
262
263        assert_eq!(dropped_counter.load(Ordering::Relaxed), 0);
264
265        // Binding another set of mock operations should cause the previous ones
266        // to be dropped.
267        bind_to_fd_with_ops(
268            MockFdOps {
269                dropped_counter: dropped_counter.clone(),
270                writev_cb: None,
271                isatty_cb: None,
272            },
273            1,
274        )
275        .unwrap();
276
277        assert_eq!(dropped_counter.load(Ordering::Relaxed), 1);
278    }
279
280    #[fuchsia::test]
281    fn test_bind_failure() {
282        let dropped_counter = Arc::new(AtomicUsize::new(0));
283        assert_eq!(
284            bind_to_fd_with_ops(
285                MockFdOps {
286                    dropped_counter: dropped_counter.clone(),
287                    writev_cb: None,
288                    isatty_cb: None
289                },
290                -1
291            ),
292            Err(zx::Status::INVALID_ARGS)
293        );
294        assert_eq!(dropped_counter.load(Ordering::Relaxed), 1);
295
296        assert_eq!(
297            bind_to_fd_with_ops(
298                MockFdOps {
299                    dropped_counter: dropped_counter.clone(),
300                    writev_cb: None,
301                    isatty_cb: None
302                },
303                1234
304            ),
305            Err(zx::Status::BAD_STATE)
306        );
307        assert_eq!(dropped_counter.load(Ordering::Relaxed), 2);
308    }
309}