Skip to main content

starnix_modules_android_usb/
lib.rs

1// Copyright 2026 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
5#![recursion_limit = "256"]
6
7use fidl_fuchsia_hardware_usb_policy::DeviceState;
8use starnix_core::task::dynamic_thread_spawner::SpawnRequestBuilder;
9use starnix_core::task::{CurrentTask, Kernel};
10use starnix_logging::log_error;
11
12use starnix_uapi::errors::Errno;
13use starnix_uapi::{errno, error};
14
15use fidl_fuchsia_usb_policy::{
16    ConfigurationMarker, ConfigurationSynchronousProxy, PolicyProviderMarker,
17};
18use fuchsia_component::client::{connect_to_protocol, connect_to_protocol_sync};
19use starnix_core::device::kobject::{Device, UEventAction};
20use starnix_core::device::mem::DevNull;
21use starnix_core::device::simple_device_ops;
22use starnix_core::fs::sysfs::build_device_directory;
23use starnix_core::vfs::pseudo::simple_file::{BytesFile, BytesFileOps};
24use starnix_core::vfs::{FsStr, FsString};
25use starnix_sync::{AndroidUsbCcToggleEnableLock, LockDepMutex};
26use starnix_uapi::file_mode::mode;
27use std::borrow::Cow;
28
29use std::future::Future;
30use std::sync::Arc;
31use std::sync::atomic::{AtomicU8, Ordering};
32
33#[repr(u8)]
34#[derive(Copy, Clone, Debug, PartialEq, Eq)]
35pub enum UsbGadgetState {
36    Disconnected = 0,
37    Configured = 1,
38    Connected = 2,
39    Unknown = 3,
40}
41
42impl From<u8> for UsbGadgetState {
43    fn from(val: u8) -> Self {
44        match val {
45            0 => UsbGadgetState::Disconnected,
46            1 => UsbGadgetState::Configured,
47            2 => UsbGadgetState::Connected,
48            _ => UsbGadgetState::Unknown,
49        }
50    }
51}
52
53impl UsbGadgetState {
54    pub fn to_fs_str(&self) -> &'static FsStr {
55        match self {
56            UsbGadgetState::Disconnected => b"DISCONNECTED".into(),
57            UsbGadgetState::Configured => b"CONFIGURED".into(),
58            UsbGadgetState::Connected => b"CONNECTED".into(),
59            UsbGadgetState::Unknown => b"UNKNOWN".into(),
60        }
61    }
62
63    pub fn map_from_device_state(state: DeviceState, previous: Option<Self>) -> Option<Self> {
64        match state {
65            DeviceState::NotAttached | DeviceState::Attached | DeviceState::Powered => {
66                Some(UsbGadgetState::Disconnected)
67            }
68            DeviceState::Default | DeviceState::Address => Some(UsbGadgetState::Connected),
69            DeviceState::Configured => Some(UsbGadgetState::Configured),
70            DeviceState::Suspended => previous, // No change when suspended
71            _ => {
72                log_error!("Unexpected DeviceState: {:?}", state);
73                previous
74            }
75        }
76    }
77}
78
79#[derive(Clone)]
80struct UsbStateSysfsFile {
81    state: Arc<AtomicU8>,
82}
83
84impl BytesFileOps for UsbStateSysfsFile {
85    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
86        let state_val = self.state.load(Ordering::Relaxed);
87        let state = UsbGadgetState::from(state_val);
88        let mut content = state.to_fs_str().to_vec();
89        content.push(b'\n'); // standard sysfs newline
90        Ok(Cow::Owned(content))
91    }
92}
93
94struct CcToggleEnableState {
95    /// Last value successfully applied via `fuchsia.usb.policy.Configuration`.
96    enabled: bool,
97    /// Lazily connected `fuchsia.usb.policy.Configuration` proxy, reused across writes so all
98    /// requests are serialized on a single channel.
99    config_proxy: Option<ConfigurationSynchronousProxy>,
100}
101
102struct CcToggleEnableSysfsFile {
103    /// Guards both the FIDL call and the `enabled` update so concurrent writers can't apply
104    /// `DisconnectCable`/`ConnectCable` in one order and record `enabled` in another.
105    state: LockDepMutex<CcToggleEnableState, AndroidUsbCcToggleEnableLock>,
106}
107
108impl CcToggleEnableSysfsFile {
109    fn new() -> Self {
110        Self { state: CcToggleEnableState { enabled: true, config_proxy: None }.into() }
111    }
112
113    #[cfg(test)]
114    fn new_with_proxy(config_proxy: ConfigurationSynchronousProxy) -> Self {
115        Self {
116            state: CcToggleEnableState { enabled: true, config_proxy: Some(config_proxy) }.into(),
117        }
118    }
119}
120
121impl BytesFileOps for CcToggleEnableSysfsFile {
122    fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
123        let content: &[u8] = if self.state.lock().enabled { b"1\n" } else { b"0\n" };
124        Ok(Cow::Borrowed(content))
125    }
126
127    fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
128        let input = std::str::from_utf8(&data).map_err(|_| errno!(EINVAL))?;
129        let enable = match input.trim_matches(|c: char| c.is_whitespace() || c == '\0') {
130            "0" => false,
131            "1" => true,
132            _ => return error!(EINVAL),
133        };
134
135        let mut state = self.state.lock();
136        if state.config_proxy.is_none() {
137            state.config_proxy =
138                Some(connect_to_protocol_sync::<ConfigurationMarker>().map_err(|e| {
139                    log_error!("Failed to connect to fuchsia.usb.policy.Configuration: {:?}", e);
140                    errno!(EIO)
141                })?);
142        }
143        let Some(proxy) = state.config_proxy.as_ref() else {
144            return error!(EIO);
145        };
146
147        let (method, result) = if enable {
148            ("ConnectCable", proxy.connect_cable(zx::MonotonicInstant::INFINITE))
149        } else {
150            ("DisconnectCable", proxy.disconnect_cable(0, zx::MonotonicInstant::INFINITE))
151        };
152        match result {
153            Ok(Ok(())) => {}
154            Ok(Err(status)) => {
155                log_error!("{method} returned error: {:?}", zx::Status::err_from_raw(status));
156                return error!(EIO);
157            }
158            Err(e) => {
159                log_error!("{method} FIDL error: {:?}", e);
160                // Drop the proxy so the next write reconnects (e.g. if usb-policy restarted).
161                state.config_proxy = None;
162                return error!(EIO);
163            }
164        }
165
166        state.enabled = enable;
167        Ok(())
168    }
169}
170
171pub fn usb_device_init(kernel: &Arc<Kernel>) -> Result<Device, Errno> {
172    let registry = &kernel.device_registry;
173
174    // TODO(https://fxbug.dev/452096300): Populate or remove stub udc class.
175    registry.objects.get_or_create_class("udc".into());
176    let android_usb_class = registry.objects.get_or_create_class("android_usb".into());
177
178    let shared_state = Arc::new(AtomicU8::new(UsbGadgetState::Disconnected as u8));
179    let state_clone = shared_state.clone();
180
181    let device = registry.add_dyn_device_with_dir(
182        kernel,
183        "android0".into(),
184        android_usb_class,
185        |device, dir| {
186            build_device_directory(device, dir);
187            dir.entry(
188                "state",
189                BytesFile::new_node(UsbStateSysfsFile { state: state_clone }),
190                mode!(IFREG, 0o444),
191            );
192        },
193        simple_device_ops::<DevNull>,
194    )?;
195
196    // Register `/sys/devices/platform/10d50000.hsi2c/i2c-6/i2c-max77759tcpc/cc_toggle_enable` so
197    // Android's `cable_breaker` tool can simulate Type-C cable disconnects and reconnects via
198    // `fuchsia.usb.policy.Configuration`.
199    // TODO(https://fxbug.dev/452096300): Drive the hardcoded `10d50000.hsi2c/i2c-6/i2c-max77759tcpc`
200    // topology from board configuration rather than registering it for all `android_usb`
201    // containers.
202    let platform_bus = registry.objects.platform_bus();
203    let i2c_bus = registry.objects.get_or_create_bus("i2c".into());
204    let hsi2c = registry.add_bus_device(
205        "10d50000.hsi2c".into(),
206        Some(registry.objects.platform_device()),
207        platform_bus,
208        build_device_directory,
209    );
210    let i2c_6 = registry.add_bus_device(
211        "i2c-6".into(),
212        Some(hsi2c),
213        i2c_bus.clone(),
214        build_device_directory,
215    );
216    registry.add_bus_device("i2c-max77759tcpc".into(), Some(i2c_6), i2c_bus, |device, dir| {
217        build_device_directory(device, dir);
218        dir.entry(
219            "cc_toggle_enable",
220            BytesFile::new_node(CcToggleEnableSysfsFile::new()),
221            mode!(IFREG, 0o644),
222        );
223    });
224
225    let kernel_clone = Arc::clone(kernel);
226    let device_clone = device.clone();
227    kernel.kthreads.spawn_future(
228        move || async move {
229            monitor_usb_device_state(kernel_clone, device_clone, shared_state).await;
230        },
231        "usb_device_state_monitor",
232    );
233
234    Ok(device)
235}
236
237// Prepare a request to broadcast a USB state change and dispatch it on a kernel thread.
238fn dispatch_usb_state_change(
239    kernel: &Arc<Kernel>,
240    device: &Device,
241    usb_state: FsString,
242) -> impl Future<Output = Result<(), Errno>> {
243    let spawner = kernel.kthreads.spawner();
244    let device_clone = device.clone();
245    let closure = move |current_task: &CurrentTask| {
246        if let Some(metadata) = device_clone.metadata() {
247            metadata.properties.insert("USB_STATE".into(), usb_state);
248        }
249
250        current_task.kernel.device_registry.dispatch_uevent(UEventAction::Change, device_clone);
251    };
252    let (result, request) =
253        SpawnRequestBuilder::new().with_sync_closure(closure).build_with_async_result();
254    spawner.spawn_from_request(request);
255    result
256}
257
258/// Monitor the USB state via FIDL messages from the PolicyProvider and dispatch uevents when the
259/// state changes.
260async fn monitor_usb_device_state(
261    kernel: Arc<Kernel>,
262    device: Device,
263    shared_state: Arc<AtomicU8>,
264) {
265    let provider = connect_to_protocol::<PolicyProviderMarker>()
266        .expect("USB Failed to connect to PolicyProvider");
267    let mut previous_mapped_state: Option<UsbGadgetState> = None;
268    loop {
269        match provider.watch_device_state().await {
270            Ok(Ok(update)) => {
271                let state = update.state.unwrap_or_else(DeviceState::unknown);
272
273                // Map the incoming device state onto the new UsbGadgetState.
274                let mapped = UsbGadgetState::map_from_device_state(state, previous_mapped_state);
275
276                if previous_mapped_state != mapped {
277                    if let Some(val) = mapped {
278                        previous_mapped_state = Some(val);
279                        shared_state.store(val as u8, Ordering::Relaxed);
280                        if let Err(e) =
281                            dispatch_usb_state_change(&kernel, &device, val.to_fs_str().to_owned())
282                                .await
283                        {
284                            log_error!("Failed to dispatch USB state change: {:?}", e);
285                        }
286                    }
287                }
288            }
289            Ok(Err(err)) => {
290                log_error!("USB PolicyProvider returned error: {:?}", err);
291                break;
292            }
293            Err(e) => {
294                log_error!("USB PolicyProvider watch failed: {:?}", e);
295                break;
296            }
297        }
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use fidl_fuchsia_usb_policy::ConfigurationRequest;
305    use futures::StreamExt;
306    use starnix_core::testing::spawn_kernel_and_run;
307    use starnix_rcu::RcuReadScope;
308
309    #[::fuchsia::test]
310    async fn test_usb_gadget_state_map_from_device_state() {
311        assert_eq!(
312            UsbGadgetState::map_from_device_state(DeviceState::NotAttached, None),
313            Some(UsbGadgetState::Disconnected)
314        );
315        assert_eq!(
316            UsbGadgetState::map_from_device_state(DeviceState::Configured, None),
317            Some(UsbGadgetState::Configured)
318        );
319        assert_eq!(
320            UsbGadgetState::map_from_device_state(DeviceState::Attached, None),
321            Some(UsbGadgetState::Disconnected)
322        );
323        assert_eq!(
324            UsbGadgetState::map_from_device_state(DeviceState::Powered, None),
325            Some(UsbGadgetState::Disconnected)
326        );
327        assert_eq!(
328            UsbGadgetState::map_from_device_state(DeviceState::Default, None),
329            Some(UsbGadgetState::Connected)
330        );
331        assert_eq!(
332            UsbGadgetState::map_from_device_state(DeviceState::Address, None),
333            Some(UsbGadgetState::Connected)
334        );
335        assert_eq!(
336            UsbGadgetState::map_from_device_state(
337                DeviceState::Suspended,
338                Some(UsbGadgetState::Configured)
339            ),
340            Some(UsbGadgetState::Configured)
341        );
342        assert_eq!(UsbGadgetState::map_from_device_state(DeviceState::Suspended, None), None);
343        assert_eq!(UsbGadgetState::map_from_device_state(DeviceState::unknown(), None), None);
344        assert_eq!(
345            UsbGadgetState::map_from_device_state(
346                DeviceState::unknown(),
347                Some(UsbGadgetState::Configured)
348            ),
349            Some(UsbGadgetState::Configured)
350        );
351    }
352
353    #[::fuchsia::test]
354    async fn test_usb_sysfs_state_reads() {
355        spawn_kernel_and_run(async |current_task| {
356            let shared_state = Arc::new(AtomicU8::new(UsbGadgetState::Disconnected as u8));
357
358            shared_state.store(UsbGadgetState::Configured as u8, Ordering::Relaxed);
359
360            let sysfs_file = UsbStateSysfsFile { state: shared_state };
361            let content = sysfs_file.read(current_task).expect("read failed");
362            assert_eq!(content.as_ref(), b"CONFIGURED\n");
363        })
364        .await;
365    }
366
367    #[::fuchsia::test]
368    async fn test_usb_device_state_to_fs_str() {
369        assert_eq!(UsbGadgetState::Disconnected.to_fs_str(), b"DISCONNECTED");
370        assert_eq!(UsbGadgetState::Configured.to_fs_str(), b"CONFIGURED");
371        assert_eq!(UsbGadgetState::Connected.to_fs_str(), b"CONNECTED");
372        assert_eq!(UsbGadgetState::Unknown.to_fs_str(), b"UNKNOWN");
373    }
374
375    #[::fuchsia::test]
376    async fn test_usb_device_state_from_u8() {
377        assert_eq!(UsbGadgetState::from(0), UsbGadgetState::Disconnected);
378        assert_eq!(UsbGadgetState::from(1), UsbGadgetState::Configured);
379        assert_eq!(UsbGadgetState::from(2), UsbGadgetState::Connected);
380        assert_eq!(UsbGadgetState::from(255), UsbGadgetState::Unknown);
381    }
382
383    #[::fuchsia::test]
384    async fn test_usb_device_init_sysfs() {
385        spawn_kernel_and_run(async |current_task| {
386            let device = usb_device_init(current_task.kernel()).expect("usb_device_init failed");
387
388            assert_eq!(device.name(), "android0");
389            let metadata = device.metadata().expect("metadata not found");
390            assert_eq!(metadata.devname.as_slice(), b"android0");
391
392            let root = &current_task.kernel().device_registry.objects.root;
393            assert!(root.lookup("class/udc".into()).is_some());
394            assert!(
395                root.lookup(
396                    "devices/platform/10d50000.hsi2c/i2c-6/i2c-max77759tcpc/cc_toggle_enable"
397                        .into()
398                )
399                .is_some()
400            );
401        })
402        .await;
403    }
404
405    #[::fuchsia::test]
406    async fn test_usb_device_metadata_property_injection() {
407        spawn_kernel_and_run(async |current_task| {
408            let device = usb_device_init(current_task.kernel()).expect("usb_device_init failed");
409
410            let kernel = current_task.kernel();
411
412            // Send the state change and wait for the background task to execute.
413            dispatch_usb_state_change(
414                kernel,
415                &device,
416                UsbGadgetState::Connected.to_fs_str().to_owned(),
417            )
418            .await
419            .unwrap();
420
421            let metadata = device.metadata().expect("metadata not found");
422            let scope = RcuReadScope::new();
423            let value = metadata
424                .properties
425                .get(&scope, FsStr::new(b"USB_STATE"))
426                .expect("property not found");
427            assert_eq!(value.as_slice(), b"CONNECTED");
428        })
429        .await;
430    }
431
432    #[::fuchsia::test]
433    async fn test_cc_toggle_enable_sysfs_read_and_write() {
434        let (config_proxy, mut config_stream) =
435            fidl::endpoints::create_sync_proxy_and_stream::<ConfigurationMarker>();
436
437        let server_task = fuchsia_async::Task::spawn(async move {
438            let mut requests = Vec::new();
439            while let Some(Ok(req)) = config_stream.next().await {
440                match req {
441                    ConfigurationRequest::DisconnectCable { duration, responder } => {
442                        requests.push((false, duration));
443                        let _ = responder.send(Ok(()));
444                    }
445                    ConfigurationRequest::ConnectCable { responder } => {
446                        requests.push((true, 0));
447                        let _ = responder.send(Ok(()));
448                    }
449                    _ => panic!("unexpected request: {:?}", req),
450                }
451                if requests.len() == 4 {
452                    break;
453                }
454            }
455            requests
456        });
457
458        spawn_kernel_and_run(async move |current_task| {
459            let sysfs_file = CcToggleEnableSysfsFile::new_with_proxy(config_proxy);
460            assert_eq!(sysfs_file.read(current_task).expect("read failed").as_ref(), b"1\n");
461
462            sysfs_file.write(current_task, b"0\n".to_vec()).expect("write 0 failed");
463            assert_eq!(sysfs_file.read(current_task).expect("read failed").as_ref(), b"0\n");
464
465            sysfs_file.write(current_task, b"1".to_vec()).expect("write 1 failed");
466            assert_eq!(sysfs_file.read(current_task).expect("read failed").as_ref(), b"1\n");
467
468            sysfs_file.write(current_task, b"0\0".to_vec()).expect("write 0\\0 failed");
469            assert_eq!(sysfs_file.read(current_task).expect("read failed").as_ref(), b"0\n");
470
471            sysfs_file.write(current_task, b"1\n\0".to_vec()).expect("write 1\\n\\0 failed");
472            assert_eq!(sysfs_file.read(current_task).expect("read failed").as_ref(), b"1\n");
473
474            assert_eq!(sysfs_file.write(current_task, b"invalid".to_vec()), error!(EINVAL));
475        })
476        .await;
477
478        assert_eq!(server_task.await, vec![(false, 0), (true, 0), (false, 0), (true, 0)]);
479    }
480
481    #[::fuchsia::test]
482    async fn test_cc_toggle_enable_sysfs_write_error_keeps_state() {
483        let (config_proxy, mut config_stream) =
484            fidl::endpoints::create_sync_proxy_and_stream::<ConfigurationMarker>();
485
486        let server_task = fuchsia_async::Task::spawn(async move {
487            let mut num_requests = 0;
488            while let Some(Ok(req)) = config_stream.next().await {
489                match req {
490                    ConfigurationRequest::DisconnectCable { responder, .. } => {
491                        let _ = responder.send(Err(zx::Status::UNAVAILABLE.into_raw()));
492                    }
493                    ConfigurationRequest::ConnectCable { responder } => {
494                        let _ = responder.send(Err(zx::Status::INTERNAL.into_raw()));
495                    }
496                    _ => panic!("unexpected request: {:?}", req),
497                }
498                num_requests += 1;
499                if num_requests == 2 {
500                    break;
501                }
502            }
503            num_requests
504        });
505
506        spawn_kernel_and_run(async move |current_task| {
507            let sysfs_file = CcToggleEnableSysfsFile::new_with_proxy(config_proxy);
508
509            // A failed DisconnectCable must return EIO and leave `enabled` set.
510            assert_eq!(sysfs_file.write(current_task, b"0".to_vec()), error!(EIO));
511            assert_eq!(sysfs_file.read(current_task).expect("read failed").as_ref(), b"1\n");
512
513            // Force `enabled` to false, then verify a failed ConnectCable leaves it unset.
514            sysfs_file.state.lock().enabled = false;
515            assert_eq!(sysfs_file.write(current_task, b"1".to_vec()), error!(EIO));
516            assert_eq!(sysfs_file.read(current_task).expect("read failed").as_ref(), b"0\n");
517        })
518        .await;
519
520        assert_eq!(server_task.await, 2);
521    }
522}