1 // Copyright 2017 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 //! Runs hardware devices in child processes.
6
7 use std::fs;
8
9 use anyhow::anyhow;
10 use base::error;
11 use base::info;
12 use base::linux::process::fork_process;
13 use base::AsRawDescriptor;
14 #[cfg(feature = "swap")]
15 use base::AsRawDescriptors;
16 use base::RawDescriptor;
17 use base::SharedMemory;
18 use base::Tube;
19 use base::TubeError;
20 use libc::pid_t;
21 use minijail::Minijail;
22 use remain::sorted;
23 use serde::Deserialize;
24 use serde::Serialize;
25 use thiserror::Error;
26
27 use crate::bus::ConfigWriteResult;
28 use crate::pci::CrosvmDeviceId;
29 use crate::pci::PciAddress;
30 use crate::BusAccessInfo;
31 use crate::BusDevice;
32 use crate::BusRange;
33 use crate::BusType;
34 use crate::DeviceId;
35 use crate::Suspendable;
36
37 /// Errors for proxy devices.
38 #[sorted]
39 #[derive(Error, Debug)]
40 pub enum Error {
41 #[error("Failed to activate ProxyDevice")]
42 ActivatingProxyDevice,
43 #[error("Failed to fork jail process: {0}")]
44 ForkingJail(#[from] minijail::Error),
45 #[error("Failed to configure swap: {0}")]
46 Swap(anyhow::Error),
47 #[error("Failed to configure tube: {0}")]
48 Tube(#[from] TubeError),
49 }
50
51 pub type Result<T> = std::result::Result<T, Error>;
52
53 #[derive(Debug, Serialize, Deserialize)]
54 enum Command {
55 Activate,
56 Read {
57 len: u32,
58 info: BusAccessInfo,
59 },
60 Write {
61 len: u32,
62 info: BusAccessInfo,
63 data: [u8; 8],
64 },
65 ReadConfig(u32),
66 WriteConfig {
67 reg_idx: u32,
68 offset: u32,
69 len: u32,
70 data: [u8; 4],
71 },
72 InitPciConfigMapping {
73 shmem: SharedMemory,
74 base: usize,
75 len: usize,
76 },
77 ReadVirtualConfig(u32),
78 WriteVirtualConfig {
79 reg_idx: u32,
80 value: u32,
81 },
82 DestroyDevice,
83 Shutdown,
84 GetRanges,
85 Snapshot,
86 Restore {
87 data: serde_json::Value,
88 },
89 Sleep,
90 Wake,
91 }
92 #[derive(Debug, Serialize, Deserialize)]
93 enum CommandResult {
94 Ok,
95 ReadResult([u8; 8]),
96 ReadConfigResult(u32),
97 WriteConfigResult {
98 mmio_remove: Vec<BusRange>,
99 mmio_add: Vec<BusRange>,
100 io_remove: Vec<BusRange>,
101 io_add: Vec<BusRange>,
102 removed_pci_devices: Vec<PciAddress>,
103 },
104 InitPciConfigMappingResult(bool),
105 ReadVirtualConfigResult(u32),
106 GetRangesResult(Vec<(BusRange, BusType)>),
107 SnapshotResult(std::result::Result<serde_json::Value, String>),
108 RestoreResult(std::result::Result<(), String>),
109 SleepResult(std::result::Result<(), String>),
110 WakeResult(std::result::Result<(), String>),
111 }
112
child_proc<D: BusDevice>(tube: Tube, mut device: D)113 fn child_proc<D: BusDevice>(tube: Tube, mut device: D) {
114 // Wait for activation signal to function as BusDevice.
115 match tube.recv() {
116 Ok(Command::Activate) => {
117 if let Err(e) = tube.send(&CommandResult::Ok) {
118 error!("sending activation result failed: {:?}", &e);
119 return;
120 }
121 }
122 // Commands other than activate is unexpected, close device.
123 Ok(cmd) => {
124 panic!("Receiving Command {:?} before device is activated", &cmd);
125 }
126 // Most likely tube error is caused by other end is dropped, release resource.
127 Err(e) => {
128 error!("device failed before activation: {:?}. Dropping device", e);
129 drop(device);
130 return;
131 }
132 };
133 loop {
134 let cmd = match tube.recv() {
135 Ok(cmd) => cmd,
136 Err(err) => {
137 error!("child device process failed recv: {}", err);
138 break;
139 }
140 };
141
142 let res = match cmd {
143 Command::Activate => {
144 panic!("Device shall only be activated once, duplicated ProxyDevice likely");
145 }
146 Command::Read { len, info } => {
147 let mut buffer = [0u8; 8];
148 device.read(info, &mut buffer[0..len as usize]);
149 tube.send(&CommandResult::ReadResult(buffer))
150 }
151 Command::Write { len, info, data } => {
152 let len = len as usize;
153 device.write(info, &data[0..len]);
154 // Command::Write does not have a result.
155 Ok(())
156 }
157 Command::ReadConfig(idx) => {
158 let val = device.config_register_read(idx as usize);
159 tube.send(&CommandResult::ReadConfigResult(val))
160 }
161 Command::WriteConfig {
162 reg_idx,
163 offset,
164 len,
165 data,
166 } => {
167 let len = len as usize;
168 let res =
169 device.config_register_write(reg_idx as usize, offset as u64, &data[0..len]);
170 tube.send(&CommandResult::WriteConfigResult {
171 mmio_remove: res.mmio_remove,
172 mmio_add: res.mmio_add,
173 io_remove: res.io_remove,
174 io_add: res.io_add,
175 removed_pci_devices: res.removed_pci_devices,
176 })
177 }
178 Command::InitPciConfigMapping { shmem, base, len } => {
179 let success = device.init_pci_config_mapping(&shmem, base, len);
180 tube.send(&CommandResult::InitPciConfigMappingResult(success))
181 }
182 Command::ReadVirtualConfig(idx) => {
183 let val = device.virtual_config_register_read(idx as usize);
184 tube.send(&CommandResult::ReadVirtualConfigResult(val))
185 }
186 Command::WriteVirtualConfig { reg_idx, value } => {
187 device.virtual_config_register_write(reg_idx as usize, value);
188 tube.send(&CommandResult::Ok)
189 }
190 Command::DestroyDevice => {
191 device.destroy_device();
192 Ok(())
193 }
194 Command::Shutdown => {
195 // Explicitly drop the device so that its Drop implementation has a chance to run
196 // before sending the `Command::Shutdown` response.
197 drop(device);
198
199 let _ = tube.send(&CommandResult::Ok);
200 return;
201 }
202 Command::GetRanges => {
203 let ranges = device.get_ranges();
204 tube.send(&CommandResult::GetRangesResult(ranges))
205 }
206 Command::Snapshot => {
207 let res = device.snapshot();
208 tube.send(&CommandResult::SnapshotResult(
209 res.map_err(|e| e.to_string()),
210 ))
211 }
212 Command::Restore { data } => {
213 let res = device.restore(data);
214 tube.send(&CommandResult::RestoreResult(
215 res.map_err(|e| e.to_string()),
216 ))
217 }
218 Command::Sleep => {
219 let res = device.sleep();
220 tube.send(&CommandResult::SleepResult(res.map_err(|e| e.to_string())))
221 }
222 Command::Wake => {
223 let res = device.wake();
224 tube.send(&CommandResult::WakeResult(res.map_err(|e| e.to_string())))
225 }
226 };
227 if let Err(e) = res {
228 error!("child device process failed send: {}", e);
229 }
230 }
231 }
232
233 /// ChildProcIntf is the interface to the device child process.
234 ///
235 /// ChildProcIntf implements Serialize, and can be sent across process before it functions as a
236 /// ProxyDevice. However, a child process shall only correspond to one ProxyDevice. The uniqueness
237 /// is checked when ChildProcIntf is casted into ProxyDevice.
238 #[derive(Serialize, Deserialize)]
239 pub struct ChildProcIntf {
240 tube: Tube,
241 pid: pid_t,
242 debug_label: String,
243 }
244
245 impl ChildProcIntf {
246 /// Creates ChildProcIntf that shall be turned into exactly one ProxyDevice.
247 ///
248 /// The ChildProcIntf struct holds the interface to the device process. It shall be turned into
249 /// a ProxyDevice exactly once (at an arbitrary process). Since ChildProcIntf may be duplicated
250 /// by serde, the uniqueness of the interface is checked when ChildProcIntf is converted into
251 /// ProxyDevice.
252 ///
253 /// # Arguments
254 /// * `device` - The device to isolate to another process.
255 /// * `jail` - The jail to use for isolating the given device.
256 /// * `keep_rds` - File descriptors that will be kept open in the child.
new<D: BusDevice, #[cfg(feature = "swap")] P: swap::PrepareFork>( mut device: D, jail: Minijail, mut keep_rds: Vec<RawDescriptor>, #[cfg(feature = "swap")] swap_prepare_fork: &mut Option<P>, ) -> Result<ChildProcIntf>257 pub fn new<D: BusDevice, #[cfg(feature = "swap")] P: swap::PrepareFork>(
258 mut device: D,
259 jail: Minijail,
260 mut keep_rds: Vec<RawDescriptor>,
261 #[cfg(feature = "swap")] swap_prepare_fork: &mut Option<P>,
262 ) -> Result<ChildProcIntf> {
263 let debug_label = device.debug_label();
264 let (child_tube, parent_tube) = Tube::pair()?;
265
266 keep_rds.push(child_tube.as_raw_descriptor());
267
268 #[cfg(feature = "swap")]
269 let swap_device_uffd_sender = if let Some(prepare_fork) = swap_prepare_fork {
270 let sender = prepare_fork.prepare_fork().map_err(Error::Swap)?;
271 keep_rds.extend(sender.as_raw_descriptors());
272 Some(sender)
273 } else {
274 None
275 };
276
277 // This will be removed after b/183540186 gets fixed.
278 // Only enabled it for x86_64 since the original bug mostly happens on x86 boards.
279 if cfg!(target_arch = "x86_64") && debug_label == "pcivirtio-gpu" {
280 if let Ok(cmd) = fs::read_to_string("/proc/self/cmdline") {
281 if cmd.contains("arcvm") {
282 if let Ok(share) = fs::read_to_string("/sys/fs/cgroup/cpu/arcvm/cpu.shares") {
283 info!("arcvm cpu share when booting gpu is {:}", share.trim());
284 }
285 }
286 }
287 }
288
289 let child_process = fork_process(jail, keep_rds, Some(debug_label.clone()), || {
290 #[cfg(feature = "swap")]
291 if let Some(swap_device_uffd_sender) = swap_device_uffd_sender {
292 if let Err(e) = swap_device_uffd_sender.on_process_forked() {
293 error!("failed to SwapController::on_process_forked: {:?}", e);
294 // SAFETY:
295 // exit() is trivially safe.
296 unsafe { libc::exit(1) };
297 }
298 }
299
300 device.on_sandboxed();
301 child_proc(child_tube, device);
302
303 // We're explicitly not using std::process::exit here to avoid the cleanup of
304 // stdout/stderr globals. This can cause cascading panics and SIGILL if a worker
305 // thread attempts to log to stderr after at_exit handlers have been run.
306 // TODO(crbug.com/992494): Remove this once device shutdown ordering is clearly
307 // defined.
308 //
309 // SAFETY:
310 // exit() is trivially safe.
311 // ! Never returns
312 unsafe { libc::exit(0) };
313 })?;
314
315 // Suppress the no waiting warning from `base::sys::linux::process::Child` because crosvm
316 // does not wait for the processes from ProxyDevice explicitly. Instead it reaps all the
317 // child processes on its exit by `crosvm::sys::linux::main::wait_all_children()`.
318 let pid = child_process.into_pid();
319
320 Ok(ChildProcIntf {
321 tube: parent_tube,
322 pid,
323 debug_label,
324 })
325 }
326 }
327
328 /// Wraps an inner `BusDevice` that is run inside a child process via fork.
329 ///
330 /// The forked device process will automatically be terminated when this is dropped.
331 pub struct ProxyDevice {
332 child_proc_intf: ChildProcIntf,
333 }
334
335 impl TryFrom<ChildProcIntf> for ProxyDevice {
336 type Error = Error;
try_from(child_proc_intf: ChildProcIntf) -> Result<Self>337 fn try_from(child_proc_intf: ChildProcIntf) -> Result<Self> {
338 // Notify child process to be activated as a BusDevice.
339 child_proc_intf.tube.send(&Command::Activate)?;
340 // Device returns Ok if it is activated only once.
341 match child_proc_intf.tube.recv()? {
342 CommandResult::Ok => Ok(Self { child_proc_intf }),
343 _ => Err(Error::ActivatingProxyDevice),
344 }
345 }
346 }
347
348 impl ProxyDevice {
349 /// Takes the given device and isolates it into another process via fork before returning.
350 ///
351 /// Because forks are very unfriendly to destructors and all memory mappings and file
352 /// descriptors are inherited, this should be used as early as possible in the main process.
353 /// ProxyDevice::new shall not be used for hotplugging. Call ChildProcIntf::new on jail warden
354 /// process, send using serde, then cast into ProxyDevice instead.
355 ///
356 /// # Arguments
357 /// * `device` - The device to isolate to another process.
358 /// * `jail` - The jail to use for isolating the given device.
359 /// * `keep_rds` - File descriptors that will be kept open in the child.
new<D: BusDevice, #[cfg(feature = "swap")] P: swap::PrepareFork>( device: D, jail: Minijail, keep_rds: Vec<RawDescriptor>, #[cfg(feature = "swap")] swap_prepare_fork: &mut Option<P>, ) -> Result<ProxyDevice>360 pub fn new<D: BusDevice, #[cfg(feature = "swap")] P: swap::PrepareFork>(
361 device: D,
362 jail: Minijail,
363 keep_rds: Vec<RawDescriptor>,
364 #[cfg(feature = "swap")] swap_prepare_fork: &mut Option<P>,
365 ) -> Result<ProxyDevice> {
366 ChildProcIntf::new(
367 device,
368 jail,
369 keep_rds,
370 #[cfg(feature = "swap")]
371 swap_prepare_fork,
372 )?
373 .try_into()
374 }
375
pid(&self) -> pid_t376 pub fn pid(&self) -> pid_t {
377 self.child_proc_intf.pid
378 }
379
380 /// Send a command that does not expect a response from the child device process.
send_no_result(&self, cmd: &Command)381 fn send_no_result(&self, cmd: &Command) {
382 let res = self.child_proc_intf.tube.send(cmd);
383 if let Err(e) = res {
384 error!(
385 "failed write to child device process {}: {}",
386 self.child_proc_intf.debug_label, e,
387 );
388 }
389 }
390
391 /// Send a command and read its response from the child device process.
sync_send(&self, cmd: &Command) -> Option<CommandResult>392 fn sync_send(&self, cmd: &Command) -> Option<CommandResult> {
393 self.send_no_result(cmd);
394 match self.child_proc_intf.tube.recv() {
395 Err(e) => {
396 error!(
397 "failed to read result of {:?} from child device process {}: {}",
398 cmd, self.child_proc_intf.debug_label, e,
399 );
400 None
401 }
402 Ok(r) => Some(r),
403 }
404 }
405 }
406
407 impl BusDevice for ProxyDevice {
device_id(&self) -> DeviceId408 fn device_id(&self) -> DeviceId {
409 CrosvmDeviceId::ProxyDevice.into()
410 }
411
debug_label(&self) -> String412 fn debug_label(&self) -> String {
413 self.child_proc_intf.debug_label.clone()
414 }
415
config_register_write( &mut self, reg_idx: usize, offset: u64, data: &[u8], ) -> ConfigWriteResult416 fn config_register_write(
417 &mut self,
418 reg_idx: usize,
419 offset: u64,
420 data: &[u8],
421 ) -> ConfigWriteResult {
422 let len = data.len() as u32;
423 let mut buffer = [0u8; 4];
424 buffer[0..data.len()].clone_from_slice(data);
425 let reg_idx = reg_idx as u32;
426 let offset = offset as u32;
427 if let Some(CommandResult::WriteConfigResult {
428 mmio_remove,
429 mmio_add,
430 io_remove,
431 io_add,
432 removed_pci_devices,
433 }) = self.sync_send(&Command::WriteConfig {
434 reg_idx,
435 offset,
436 len,
437 data: buffer,
438 }) {
439 ConfigWriteResult {
440 mmio_remove,
441 mmio_add,
442 io_remove,
443 io_add,
444 removed_pci_devices,
445 }
446 } else {
447 Default::default()
448 }
449 }
450
config_register_read(&self, reg_idx: usize) -> u32451 fn config_register_read(&self, reg_idx: usize) -> u32 {
452 let res = self.sync_send(&Command::ReadConfig(reg_idx as u32));
453 if let Some(CommandResult::ReadConfigResult(val)) = res {
454 val
455 } else {
456 0
457 }
458 }
459
init_pci_config_mapping(&mut self, shmem: &SharedMemory, base: usize, len: usize) -> bool460 fn init_pci_config_mapping(&mut self, shmem: &SharedMemory, base: usize, len: usize) -> bool {
461 let Ok(shmem) = shmem.try_clone() else {
462 error!("Failed to clone pci config mapping shmem");
463 return false;
464 };
465 let res = self.sync_send(&Command::InitPciConfigMapping { shmem, base, len });
466 matches!(res, Some(CommandResult::InitPciConfigMappingResult(true)))
467 }
468
virtual_config_register_write(&mut self, reg_idx: usize, value: u32)469 fn virtual_config_register_write(&mut self, reg_idx: usize, value: u32) {
470 let reg_idx = reg_idx as u32;
471 self.sync_send(&Command::WriteVirtualConfig { reg_idx, value });
472 }
473
virtual_config_register_read(&self, reg_idx: usize) -> u32474 fn virtual_config_register_read(&self, reg_idx: usize) -> u32 {
475 let res = self.sync_send(&Command::ReadVirtualConfig(reg_idx as u32));
476 if let Some(CommandResult::ReadVirtualConfigResult(val)) = res {
477 val
478 } else {
479 0
480 }
481 }
482
read(&mut self, info: BusAccessInfo, data: &mut [u8])483 fn read(&mut self, info: BusAccessInfo, data: &mut [u8]) {
484 let len = data.len() as u32;
485 if let Some(CommandResult::ReadResult(buffer)) =
486 self.sync_send(&Command::Read { len, info })
487 {
488 let len = data.len();
489 data.clone_from_slice(&buffer[0..len]);
490 }
491 }
492
write(&mut self, info: BusAccessInfo, data: &[u8])493 fn write(&mut self, info: BusAccessInfo, data: &[u8]) {
494 let mut buffer = [0u8; 8];
495 let len = data.len() as u32;
496 buffer[0..data.len()].clone_from_slice(data);
497 self.send_no_result(&Command::Write {
498 len,
499 info,
500 data: buffer,
501 });
502 }
503
get_ranges(&self) -> Vec<(BusRange, BusType)>504 fn get_ranges(&self) -> Vec<(BusRange, BusType)> {
505 if let Some(CommandResult::GetRangesResult(ranges)) = self.sync_send(&Command::GetRanges) {
506 ranges
507 } else {
508 Default::default()
509 }
510 }
511
destroy_device(&mut self)512 fn destroy_device(&mut self) {
513 self.send_no_result(&Command::DestroyDevice);
514 }
515 }
516
517 impl Suspendable for ProxyDevice {
snapshot(&mut self) -> anyhow::Result<serde_json::Value>518 fn snapshot(&mut self) -> anyhow::Result<serde_json::Value> {
519 let res = self.sync_send(&Command::Snapshot);
520 match res {
521 Some(CommandResult::SnapshotResult(Ok(snap))) => Ok(snap),
522 Some(CommandResult::SnapshotResult(Err(e))) => Err(anyhow!(
523 "failed to snapshot {}: {:#}",
524 self.debug_label(),
525 e
526 )),
527 _ => Err(anyhow!("unexpected snapshot result {:?}", res)),
528 }
529 }
530
restore(&mut self, data: serde_json::Value) -> anyhow::Result<()>531 fn restore(&mut self, data: serde_json::Value) -> anyhow::Result<()> {
532 let res = self.sync_send(&Command::Restore { data });
533 match res {
534 Some(CommandResult::RestoreResult(Ok(()))) => Ok(()),
535 Some(CommandResult::RestoreResult(Err(e))) => {
536 Err(anyhow!("failed to restore {}: {:#}", self.debug_label(), e))
537 }
538 _ => Err(anyhow!("unexpected restore result {:?}", res)),
539 }
540 }
541
sleep(&mut self) -> anyhow::Result<()>542 fn sleep(&mut self) -> anyhow::Result<()> {
543 let res = self.sync_send(&Command::Sleep);
544 match res {
545 Some(CommandResult::SleepResult(Ok(()))) => Ok(()),
546 Some(CommandResult::SleepResult(Err(e))) => {
547 Err(anyhow!("failed to sleep {}: {:#}", self.debug_label(), e))
548 }
549 _ => Err(anyhow!("unexpected sleep result {:?}", res)),
550 }
551 }
552
wake(&mut self) -> anyhow::Result<()>553 fn wake(&mut self) -> anyhow::Result<()> {
554 let res = self.sync_send(&Command::Wake);
555 match res {
556 Some(CommandResult::WakeResult(Ok(()))) => Ok(()),
557 Some(CommandResult::WakeResult(Err(e))) => {
558 Err(anyhow!("failed to wake {}: {:#}", self.debug_label(), e))
559 }
560 _ => Err(anyhow!("unexpected wake result {:?}", res)),
561 }
562 }
563 }
564
565 impl Drop for ProxyDevice {
drop(&mut self)566 fn drop(&mut self) {
567 self.sync_send(&Command::Shutdown);
568 }
569 }
570
571 /// Note: These tests must be run with --test-threads=1 to allow minijail to fork
572 /// the process.
573 #[cfg(test)]
574 mod tests {
575 use super::*;
576 use crate::pci::PciId;
577
578 /// A simple test echo device that outputs the same u8 that was written to it.
579 struct EchoDevice {
580 data: u8,
581 config: u8,
582 }
583 impl EchoDevice {
new() -> EchoDevice584 fn new() -> EchoDevice {
585 EchoDevice { data: 0, config: 0 }
586 }
587 }
588 impl BusDevice for EchoDevice {
device_id(&self) -> DeviceId589 fn device_id(&self) -> DeviceId {
590 PciId::new(0, 0).into()
591 }
592
debug_label(&self) -> String593 fn debug_label(&self) -> String {
594 "EchoDevice".to_owned()
595 }
596
write(&mut self, _info: BusAccessInfo, data: &[u8])597 fn write(&mut self, _info: BusAccessInfo, data: &[u8]) {
598 assert!(data.len() == 1);
599 self.data = data[0];
600 }
601
read(&mut self, _info: BusAccessInfo, data: &mut [u8])602 fn read(&mut self, _info: BusAccessInfo, data: &mut [u8]) {
603 assert!(data.len() == 1);
604 data[0] = self.data;
605 }
606
config_register_write( &mut self, _reg_idx: usize, _offset: u64, data: &[u8], ) -> ConfigWriteResult607 fn config_register_write(
608 &mut self,
609 _reg_idx: usize,
610 _offset: u64,
611 data: &[u8],
612 ) -> ConfigWriteResult {
613 let result = ConfigWriteResult {
614 ..Default::default()
615 };
616 assert!(data.len() == 1);
617 self.config = data[0];
618 result
619 }
620
config_register_read(&self, _reg_idx: usize) -> u32621 fn config_register_read(&self, _reg_idx: usize) -> u32 {
622 self.config as u32
623 }
624 }
625
626 impl Suspendable for EchoDevice {}
627
new_proxied_echo_device() -> ProxyDevice628 fn new_proxied_echo_device() -> ProxyDevice {
629 let device = EchoDevice::new();
630 let keep_fds: Vec<RawDescriptor> = Vec::new();
631 let minijail = Minijail::new().unwrap();
632 ProxyDevice::new(
633 device,
634 minijail,
635 keep_fds,
636 #[cfg(feature = "swap")]
637 &mut None::<swap::SwapController>,
638 )
639 .unwrap()
640 }
641
642 // TODO(b/173833661): Find a way to ensure these tests are run single-threaded.
643 #[test]
644 #[ignore]
test_debug_label()645 fn test_debug_label() {
646 let proxy_device = new_proxied_echo_device();
647 assert_eq!(proxy_device.debug_label(), "EchoDevice");
648 }
649
650 #[test]
651 #[ignore]
test_proxied_read_write()652 fn test_proxied_read_write() {
653 let mut proxy_device = new_proxied_echo_device();
654 let address = BusAccessInfo {
655 offset: 0,
656 address: 0,
657 id: 0,
658 };
659 proxy_device.write(address, &[42]);
660 let mut read_buffer = [0];
661 proxy_device.read(address, &mut read_buffer);
662 assert_eq!(read_buffer, [42]);
663 }
664
665 #[test]
666 #[ignore]
test_proxied_config()667 fn test_proxied_config() {
668 let mut proxy_device = new_proxied_echo_device();
669 proxy_device.config_register_write(0, 0, &[42]);
670 assert_eq!(proxy_device.config_register_read(0), 42);
671 }
672 }
673