devices/virtio/block/
mod.rs

1// Copyright 2021 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#[cfg(windows)]
6use std::num::NonZeroU32;
7use std::path::PathBuf;
8
9use cros_async::ExecutorKind;
10use serde::Deserialize;
11use serde::Deserializer;
12use serde::Serialize;
13use serde::Serializer;
14
15use crate::PciAddress;
16
17pub mod asynchronous;
18pub(crate) mod sys;
19
20pub use asynchronous::BlockAsync;
21
22fn block_option_sparse_default() -> bool {
23    true
24}
25fn block_option_lock_default() -> bool {
26    true
27}
28fn block_option_block_size_default() -> u32 {
29    512
30}
31// TODO(b/237829580): Move to sys module once virtio block sys is refactored to
32// match the style guide.
33#[cfg(windows)]
34fn block_option_io_concurrency_default() -> NonZeroU32 {
35    NonZeroU32::new(1).unwrap()
36}
37
38/// Maximum length of a `DiskOption` identifier.
39///
40/// This is based on the virtio-block ID length limit.
41pub const DISK_ID_LEN: usize = 20;
42
43pub fn serialize_disk_id<S: Serializer>(
44    id: &Option<[u8; DISK_ID_LEN]>,
45    serializer: S,
46) -> Result<S::Ok, S::Error> {
47    match id {
48        None => serializer.serialize_none(),
49        Some(id) => {
50            // Find the first zero byte in the id.
51            let len = id.iter().position(|v| *v == 0).unwrap_or(DISK_ID_LEN);
52            serializer.serialize_some(
53                std::str::from_utf8(&id[0..len])
54                    .map_err(|e| serde::ser::Error::custom(e.to_string()))?,
55            )
56        }
57    }
58}
59
60fn deserialize_disk_id<'de, D: Deserializer<'de>>(
61    deserializer: D,
62) -> Result<Option<[u8; DISK_ID_LEN]>, D::Error> {
63    let id = Option::<String>::deserialize(deserializer)?;
64
65    match id {
66        None => Ok(None),
67        Some(id) => {
68            if id.len() > DISK_ID_LEN {
69                return Err(serde::de::Error::custom(format!(
70                    "disk id must be {DISK_ID_LEN} or fewer characters"
71                )));
72            }
73
74            let mut ret = [0u8; DISK_ID_LEN];
75            // Slicing id to value's length will never panic
76            // because we checked that value will fit into id above.
77            ret[..id.len()].copy_from_slice(id.as_bytes());
78            Ok(Some(ret))
79        }
80    }
81}
82
83#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, serde_keyvalue::FromKeyValues)]
84#[serde(deny_unknown_fields, rename_all = "kebab-case")]
85pub struct DiskOption {
86    pub path: PathBuf,
87    #[serde(default, rename = "ro")]
88    pub read_only: bool,
89    #[serde(default)]
90    /// Whether this disk should be the root device. Can only be set once. Only useful for adding
91    /// specific command-line options.
92    pub root: bool,
93    #[serde(default = "block_option_sparse_default")]
94    pub sparse: bool,
95    // camel_case variant allowed for backward compatibility.
96    #[serde(default, alias = "o_direct")]
97    pub direct: bool,
98    /// Whether to lock the disk files. Uses flock on Unix and FILE_SHARE_* flags on Windows.
99    #[serde(default = "block_option_lock_default")]
100    pub lock: bool,
101    // camel_case variant allowed for backward compatibility.
102    #[serde(default = "block_option_block_size_default", alias = "block_size")]
103    pub block_size: u32,
104    #[serde(
105        default,
106        serialize_with = "serialize_disk_id",
107        deserialize_with = "deserialize_disk_id"
108    )]
109    pub id: Option<[u8; DISK_ID_LEN]>,
110    // Deprecated: Use async_executor=overlapped[concurrency=N]"
111    // camel_case variant allowed for backward compatibility.
112    #[cfg(windows)]
113    #[serde(
114        default = "block_option_io_concurrency_default",
115        alias = "io_concurrency"
116    )]
117    pub io_concurrency: NonZeroU32,
118    #[serde(default)]
119    /// Experimental option to run multiple worker threads in parallel. If false, only single
120    /// thread runs by default. Note this option is not effective for vhost-user blk device.
121    pub multiple_workers: bool,
122    #[serde(default, alias = "async_executor")]
123    /// The async executor kind to simulate the block device with. This option takes
124    /// precedence over the async executor kind specified by the subcommand's option.
125    /// If None, the default or the specified by the subcommand's option would be used.
126    pub async_executor: Option<ExecutorKind>,
127    #[serde(default)]
128    //Option to choose virtqueue type. If true, use the packed virtqueue. If false
129    //or by default, use split virtqueue
130    pub packed_queue: bool,
131
132    /// Specify the boot index for this device that the BIOS will use when attempting to boot from
133    /// bootable devices. For example, if bootindex=2, then the BIOS will attempt to boot from the
134    /// device right after booting from the device with bootindex=1 fails.
135    pub bootindex: Option<usize>,
136
137    /// Specify PCI address will be used to attach this device
138    pub pci_address: Option<PciAddress>,
139}
140
141impl Default for DiskOption {
142    fn default() -> Self {
143        Self {
144            path: PathBuf::new(),
145            read_only: false,
146            root: false,
147            sparse: block_option_sparse_default(),
148            direct: false,
149            lock: block_option_lock_default(),
150            block_size: block_option_block_size_default(),
151            id: None,
152            #[cfg(windows)]
153            io_concurrency: block_option_io_concurrency_default(),
154            multiple_workers: false,
155            async_executor: None,
156            packed_queue: false,
157            bootindex: None,
158            pci_address: None,
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    #[cfg(any(target_os = "android", target_os = "linux"))]
166    use cros_async::sys::linux::ExecutorKindSys;
167    #[cfg(windows)]
168    use cros_async::sys::windows::ExecutorKindSys;
169    use serde_keyvalue::*;
170
171    use super::*;
172
173    fn from_block_arg(options: &str) -> Result<DiskOption, ParseError> {
174        from_key_values(options)
175    }
176
177    #[test]
178    fn check_default_matches_from_key_values() {
179        let path = "/path/to/disk.img";
180        let disk = DiskOption {
181            path: PathBuf::from(path),
182            ..DiskOption::default()
183        };
184        assert_eq!(disk, from_key_values(path).unwrap());
185    }
186
187    #[test]
188    fn params_from_key_values() {
189        // Path argument is mandatory.
190        let err = from_block_arg("").unwrap_err();
191        assert_eq!(
192            err,
193            ParseError {
194                kind: ErrorKind::SerdeError("missing field `path`".into()),
195                pos: 0,
196            }
197        );
198
199        // Path is the default argument.
200        let params = from_block_arg("/path/to/disk.img").unwrap();
201        assert_eq!(
202            params,
203            DiskOption {
204                path: "/path/to/disk.img".into(),
205                read_only: false,
206                root: false,
207                sparse: true,
208                direct: false,
209                lock: true,
210                block_size: 512,
211                id: None,
212                #[cfg(windows)]
213                io_concurrency: NonZeroU32::new(1).unwrap(),
214                multiple_workers: false,
215                async_executor: None,
216                packed_queue: false,
217                bootindex: None,
218                pci_address: None,
219            }
220        );
221
222        // bootindex
223        let params = from_block_arg("/path/to/disk.img,bootindex=5").unwrap();
224        assert_eq!(
225            params,
226            DiskOption {
227                path: "/path/to/disk.img".into(),
228                read_only: false,
229                root: false,
230                sparse: true,
231                direct: false,
232                lock: true,
233                block_size: 512,
234                id: None,
235                #[cfg(windows)]
236                io_concurrency: NonZeroU32::new(1).unwrap(),
237                multiple_workers: false,
238                async_executor: None,
239                packed_queue: false,
240                bootindex: Some(5),
241                pci_address: None,
242            }
243        );
244
245        // Explicitly-specified path.
246        let params = from_block_arg("path=/path/to/disk.img").unwrap();
247        assert_eq!(
248            params,
249            DiskOption {
250                path: "/path/to/disk.img".into(),
251                read_only: false,
252                root: false,
253                sparse: true,
254                direct: false,
255                lock: true,
256                block_size: 512,
257                id: None,
258                #[cfg(windows)]
259                io_concurrency: NonZeroU32::new(1).unwrap(),
260                multiple_workers: false,
261                async_executor: None,
262                packed_queue: false,
263                bootindex: None,
264                pci_address: None,
265            }
266        );
267
268        // read_only
269        let params = from_block_arg("/some/path.img,ro").unwrap();
270        assert_eq!(
271            params,
272            DiskOption {
273                path: "/some/path.img".into(),
274                read_only: true,
275                root: false,
276                sparse: true,
277                direct: false,
278                lock: true,
279                block_size: 512,
280                id: None,
281                #[cfg(windows)]
282                io_concurrency: NonZeroU32::new(1).unwrap(),
283                multiple_workers: false,
284                async_executor: None,
285                packed_queue: false,
286                bootindex: None,
287                pci_address: None,
288            }
289        );
290
291        // root
292        let params = from_block_arg("/some/path.img,root").unwrap();
293        assert_eq!(
294            params,
295            DiskOption {
296                path: "/some/path.img".into(),
297                read_only: false,
298                root: true,
299                sparse: true,
300                direct: false,
301                lock: true,
302                block_size: 512,
303                id: None,
304                #[cfg(windows)]
305                io_concurrency: NonZeroU32::new(1).unwrap(),
306                multiple_workers: false,
307                async_executor: None,
308                packed_queue: false,
309                bootindex: None,
310                pci_address: None,
311            }
312        );
313
314        // sparse
315        let params = from_block_arg("/some/path.img,sparse").unwrap();
316        assert_eq!(
317            params,
318            DiskOption {
319                path: "/some/path.img".into(),
320                read_only: false,
321                root: false,
322                sparse: true,
323                direct: false,
324                lock: true,
325                block_size: 512,
326                id: None,
327                #[cfg(windows)]
328                io_concurrency: NonZeroU32::new(1).unwrap(),
329                multiple_workers: false,
330                async_executor: None,
331                packed_queue: false,
332                bootindex: None,
333                pci_address: None,
334            }
335        );
336        let params = from_block_arg("/some/path.img,sparse=false").unwrap();
337        assert_eq!(
338            params,
339            DiskOption {
340                path: "/some/path.img".into(),
341                read_only: false,
342                root: false,
343                sparse: false,
344                direct: false,
345                lock: true,
346                block_size: 512,
347                id: None,
348                #[cfg(windows)]
349                io_concurrency: NonZeroU32::new(1).unwrap(),
350                multiple_workers: false,
351                async_executor: None,
352                packed_queue: false,
353                bootindex: None,
354                pci_address: None,
355            }
356        );
357
358        // direct
359        let params = from_block_arg("/some/path.img,direct").unwrap();
360        assert_eq!(
361            params,
362            DiskOption {
363                path: "/some/path.img".into(),
364                read_only: false,
365                root: false,
366                sparse: true,
367                direct: true,
368                lock: true,
369                block_size: 512,
370                id: None,
371                #[cfg(windows)]
372                io_concurrency: NonZeroU32::new(1).unwrap(),
373                multiple_workers: false,
374                async_executor: None,
375                packed_queue: false,
376                bootindex: None,
377                pci_address: None,
378            }
379        );
380
381        // o_direct (deprecated, kept for backward compatibility)
382        let params = from_block_arg("/some/path.img,o_direct").unwrap();
383        assert_eq!(
384            params,
385            DiskOption {
386                path: "/some/path.img".into(),
387                read_only: false,
388                root: false,
389                sparse: true,
390                direct: true,
391                lock: true,
392                block_size: 512,
393                id: None,
394                #[cfg(windows)]
395                io_concurrency: NonZeroU32::new(1).unwrap(),
396                multiple_workers: false,
397                async_executor: None,
398                packed_queue: false,
399                bootindex: None,
400                pci_address: None,
401            }
402        );
403
404        // block-size
405        let params = from_block_arg("/some/path.img,block-size=128").unwrap();
406        assert_eq!(
407            params,
408            DiskOption {
409                path: "/some/path.img".into(),
410                read_only: false,
411                root: false,
412                sparse: true,
413                direct: false,
414                lock: true,
415                block_size: 128,
416                id: None,
417                #[cfg(windows)]
418                io_concurrency: NonZeroU32::new(1).unwrap(),
419                multiple_workers: false,
420                async_executor: None,
421                packed_queue: false,
422                bootindex: None,
423                pci_address: None,
424            }
425        );
426
427        // block_size (deprecated, kept for backward compatibility)
428        let params = from_block_arg("/some/path.img,block_size=128").unwrap();
429        assert_eq!(
430            params,
431            DiskOption {
432                path: "/some/path.img".into(),
433                read_only: false,
434                root: false,
435                sparse: true,
436                direct: false,
437                lock: true,
438                block_size: 128,
439                id: None,
440                async_executor: None,
441                #[cfg(windows)]
442                io_concurrency: NonZeroU32::new(1).unwrap(),
443                multiple_workers: false,
444                packed_queue: false,
445                bootindex: None,
446                pci_address: None,
447            }
448        );
449
450        // io_concurrency
451        #[cfg(windows)]
452        {
453            let params = from_block_arg("/some/path.img,io_concurrency=4").unwrap();
454            assert_eq!(
455                params,
456                DiskOption {
457                    path: "/some/path.img".into(),
458                    read_only: false,
459                    root: false,
460                    sparse: true,
461                    direct: false,
462                    lock: true,
463                    block_size: 512,
464                    id: None,
465                    io_concurrency: NonZeroU32::new(4).unwrap(),
466                    multiple_workers: false,
467                    async_executor: None,
468                    packed_queue: false,
469                    bootindex: None,
470                    pci_address: None,
471                }
472            );
473            let params = from_block_arg("/some/path.img,async-executor=overlapped").unwrap();
474            assert_eq!(
475                params,
476                DiskOption {
477                    path: "/some/path.img".into(),
478                    read_only: false,
479                    root: false,
480                    sparse: true,
481                    direct: false,
482                    lock: true,
483                    block_size: 512,
484                    id: None,
485                    io_concurrency: NonZeroU32::new(1).unwrap(),
486                    multiple_workers: false,
487                    async_executor: Some(ExecutorKindSys::Overlapped { concurrency: None }.into()),
488                    packed_queue: false,
489                    bootindex: None,
490                    pci_address: None,
491                }
492            );
493            let params =
494                from_block_arg("/some/path.img,async-executor=\"overlapped,concurrency=4\"")
495                    .unwrap();
496            assert_eq!(
497                params,
498                DiskOption {
499                    path: "/some/path.img".into(),
500                    read_only: false,
501                    root: false,
502                    sparse: true,
503                    direct: false,
504                    lock: true,
505                    block_size: 512,
506                    id: None,
507                    io_concurrency: NonZeroU32::new(1).unwrap(),
508                    multiple_workers: false,
509                    async_executor: Some(
510                        ExecutorKindSys::Overlapped {
511                            concurrency: Some(4)
512                        }
513                        .into()
514                    ),
515                    packed_queue: false,
516                    bootindex: None,
517                    pci_address: None,
518                }
519            );
520        }
521
522        // id
523        let params = from_block_arg("/some/path.img,id=DISK").unwrap();
524        assert_eq!(
525            params,
526            DiskOption {
527                path: "/some/path.img".into(),
528                read_only: false,
529                root: false,
530                sparse: true,
531                direct: false,
532                lock: true,
533                block_size: 512,
534                id: Some(*b"DISK\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"),
535                #[cfg(windows)]
536                io_concurrency: NonZeroU32::new(1).unwrap(),
537                multiple_workers: false,
538                async_executor: None,
539                packed_queue: false,
540                bootindex: None,
541                pci_address: None,
542            }
543        );
544        let err = from_block_arg("/some/path.img,id=DISK_ID_IS_WAY_TOO_LONG").unwrap_err();
545        assert_eq!(
546            err,
547            ParseError {
548                kind: ErrorKind::SerdeError("disk id must be 20 or fewer characters".into()),
549                pos: 0,
550            }
551        );
552
553        // async-executor
554        #[cfg(windows)]
555        let (ex_kind, ex_kind_opt) = (ExecutorKindSys::Handle.into(), "handle");
556        #[cfg(any(target_os = "android", target_os = "linux"))]
557        let (ex_kind, ex_kind_opt) = (ExecutorKindSys::Fd.into(), "epoll");
558        let params =
559            from_block_arg(&format!("/some/path.img,async-executor={ex_kind_opt}")).unwrap();
560        assert_eq!(
561            params,
562            DiskOption {
563                path: "/some/path.img".into(),
564                read_only: false,
565                root: false,
566                sparse: true,
567                direct: false,
568                lock: true,
569                block_size: 512,
570                id: None,
571                #[cfg(windows)]
572                io_concurrency: NonZeroU32::new(1).unwrap(),
573                multiple_workers: false,
574                async_executor: Some(ex_kind),
575                packed_queue: false,
576                bootindex: None,
577                pci_address: None,
578            }
579        );
580
581        // packed queue
582        let params = from_block_arg("/path/to/disk.img,packed-queue").unwrap();
583        assert_eq!(
584            params,
585            DiskOption {
586                path: "/path/to/disk.img".into(),
587                read_only: false,
588                root: false,
589                sparse: true,
590                direct: false,
591                lock: true,
592                block_size: 512,
593                id: None,
594                #[cfg(windows)]
595                io_concurrency: NonZeroU32::new(1).unwrap(),
596                multiple_workers: false,
597                async_executor: None,
598                packed_queue: true,
599                bootindex: None,
600                pci_address: None,
601            }
602        );
603
604        // pci-address
605        let params = from_block_arg("/path/to/disk.img,pci-address=00:01.1").unwrap();
606        assert_eq!(
607            params,
608            DiskOption {
609                path: "/path/to/disk.img".into(),
610                read_only: false,
611                root: false,
612                sparse: true,
613                direct: false,
614                lock: true,
615                block_size: 512,
616                id: None,
617                #[cfg(windows)]
618                io_concurrency: NonZeroU32::new(1).unwrap(),
619                multiple_workers: false,
620                async_executor: None,
621                packed_queue: false,
622                bootindex: None,
623                pci_address: Some(PciAddress {
624                    bus: 0,
625                    dev: 1,
626                    func: 1,
627                }),
628            }
629        );
630
631        // lock=true
632        let params = from_block_arg("/path/to/disk.img,lock=true").unwrap();
633        assert_eq!(
634            params,
635            DiskOption {
636                path: "/path/to/disk.img".into(),
637                read_only: false,
638                root: false,
639                sparse: true,
640                direct: false,
641                lock: true,
642                block_size: 512,
643                id: None,
644                #[cfg(windows)]
645                io_concurrency: NonZeroU32::new(1).unwrap(),
646                multiple_workers: false,
647                async_executor: None,
648                packed_queue: false,
649                bootindex: None,
650                pci_address: None,
651            }
652        );
653        // lock=false
654        let params = from_block_arg("/path/to/disk.img,lock=false").unwrap();
655        assert_eq!(
656            params,
657            DiskOption {
658                path: "/path/to/disk.img".into(),
659                read_only: false,
660                root: false,
661                sparse: true,
662                direct: false,
663                lock: false,
664                block_size: 512,
665                id: None,
666                #[cfg(windows)]
667                io_concurrency: NonZeroU32::new(1).unwrap(),
668                multiple_workers: false,
669                async_executor: None,
670                packed_queue: false,
671                bootindex: None,
672                pci_address: None,
673            }
674        );
675
676        // All together
677        let params = from_block_arg(&format!(
678            "/some/path.img,block_size=256,ro,root,sparse=false,id=DISK_LABEL\
679            ,direct,async-executor={ex_kind_opt},packed-queue=false,pci-address=00:01.1"
680        ))
681        .unwrap();
682        assert_eq!(
683            params,
684            DiskOption {
685                path: "/some/path.img".into(),
686                read_only: true,
687                root: true,
688                sparse: false,
689                direct: true,
690                lock: true,
691                block_size: 256,
692                id: Some(*b"DISK_LABEL\0\0\0\0\0\0\0\0\0\0"),
693                #[cfg(windows)]
694                io_concurrency: NonZeroU32::new(1).unwrap(),
695                multiple_workers: false,
696                async_executor: Some(ex_kind),
697                packed_queue: false,
698                bootindex: None,
699                pci_address: Some(PciAddress {
700                    bus: 0,
701                    dev: 1,
702                    func: 1,
703                }),
704            }
705        );
706    }
707
708    #[test]
709    fn diskoption_serialize_deserialize() {
710        // With id == None
711        let original = DiskOption {
712            path: "./rootfs".into(),
713            read_only: false,
714            root: false,
715            sparse: true,
716            direct: false,
717            lock: true,
718            block_size: 512,
719            id: None,
720            #[cfg(windows)]
721            io_concurrency: NonZeroU32::new(1).unwrap(),
722            multiple_workers: false,
723            async_executor: None,
724            packed_queue: false,
725            bootindex: None,
726            pci_address: None,
727        };
728        let json = serde_json::to_string(&original).unwrap();
729        let deserialized = serde_json::from_str(&json).unwrap();
730        assert_eq!(original, deserialized);
731
732        // With id == Some
733        let original = DiskOption {
734            path: "./rootfs".into(),
735            read_only: false,
736            root: false,
737            sparse: true,
738            direct: false,
739            lock: true,
740            block_size: 512,
741            id: Some(*b"BLK\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"),
742            #[cfg(windows)]
743            io_concurrency: NonZeroU32::new(1).unwrap(),
744            multiple_workers: false,
745            async_executor: Some(ExecutorKind::default()),
746            packed_queue: false,
747            bootindex: None,
748            pci_address: None,
749        };
750        let json = serde_json::to_string(&original).unwrap();
751        let deserialized = serde_json::from_str(&json).unwrap();
752        assert_eq!(original, deserialized);
753
754        // With id taking all the available space.
755        let original = DiskOption {
756            path: "./rootfs".into(),
757            read_only: false,
758            root: false,
759            sparse: true,
760            direct: false,
761            lock: true,
762            block_size: 512,
763            id: Some(*b"QWERTYUIOPASDFGHJKL:"),
764            #[cfg(windows)]
765            io_concurrency: NonZeroU32::new(1).unwrap(),
766            multiple_workers: false,
767            async_executor: Some(ExecutorKind::default()),
768            packed_queue: false,
769            bootindex: None,
770            pci_address: None,
771        };
772        let json = serde_json::to_string(&original).unwrap();
773        let deserialized = serde_json::from_str(&json).unwrap();
774        assert_eq!(original, deserialized);
775    }
776}