Skip to main content

virtio/
regions.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Helpers for working with data regions defined by virtio descriptors.
5
6use std::borrow::Borrow;
7
8/// A data-carrying region extracted from a descriptor chain.
9///
10/// Each entry represents one contiguous GPA range.
11pub struct DataRegion {
12    pub addr: u64,
13    pub len: u64,
14}
15
16/// Extract the data-carrying regions from a descriptor chain.
17///
18/// Returns an iterator that filters descriptors by direction (`writable`),
19/// skips `skip_bytes` (the request header for writes), and limits the
20/// total to `data_len` (which excludes the status byte for reads).
21pub fn data_regions(
22    payloads: &[crate::queue::VirtioQueuePayload],
23    writable: bool,
24    skip_bytes: u64,
25    data_len: u64,
26) -> DataRegions<'_> {
27    DataRegions {
28        payloads: payloads.iter(),
29        writable,
30        skip: skip_bytes,
31        remaining: data_len,
32    }
33}
34
35/// Iterator over data-carrying regions from a descriptor chain.
36///
37/// Created by [`data_regions`].
38pub struct DataRegions<'a> {
39    payloads: core::slice::Iter<'a, crate::queue::VirtioQueuePayload>,
40    writable: bool,
41    skip: u64,
42    remaining: u64,
43}
44
45impl Iterator for DataRegions<'_> {
46    type Item = DataRegion;
47
48    fn next(&mut self) -> Option<DataRegion> {
49        while self.remaining > 0 {
50            let payload = self.payloads.next()?;
51            if payload.writeable != self.writable {
52                continue;
53            }
54            let mut addr = payload.address;
55            let mut plen = payload.length as u64;
56            if self.skip > 0 {
57                let s = self.skip.min(plen);
58                addr += s;
59                plen -= s;
60                self.skip -= s;
61            }
62            if plen == 0 {
63                continue;
64            }
65            let chunk = plen.min(self.remaining);
66            self.remaining -= chunk;
67            return Some(DataRegion { addr, len: chunk });
68        }
69        None
70    }
71}
72
73/// Try to build a single `PagedRange` GPN list from the data regions.
74///
75/// Returns `Some((gpns, offset, len))` if every region boundary falls on
76/// a page boundary (or regions are GPA-contiguous), so the whole chain
77/// can be expressed as one [`guestmem::ranges::PagedRange`]. Returns `None` if any
78/// interior boundary violates the constraint.
79pub fn try_build_gpn_list(
80    regions: impl IntoIterator<Item = impl Borrow<DataRegion>>,
81) -> Option<(Vec<u64>, usize, usize)> {
82    const PAGE_SIZE: u64 = guestmem::PAGE_SIZE as u64;
83
84    let mut gpns = Vec::new();
85    let mut total_len: u64 = 0;
86    let mut first_offset: Option<usize> = None;
87    let mut prev_end: Option<u64> = None;
88
89    for region in regions {
90        let region = region.borrow();
91        let addr = region.addr;
92        let len = region.len;
93        if len == 0 {
94            continue;
95        }
96
97        let end = addr.checked_add(len)?;
98        let first_gpn = addr / PAGE_SIZE;
99        let last_gpn = (end - 1) / PAGE_SIZE;
100
101        if let Some(pe) = prev_end {
102            if addr == pe {
103                // GPA-contiguous with the previous region.
104                // The shared page (if any) is already in gpns.
105                let last_gpn_in_list = *gpns.last().unwrap();
106                if first_gpn == last_gpn_in_list {
107                    // Same page — just add any new pages beyond it.
108                    for gpn in (first_gpn + 1)..=last_gpn {
109                        gpns.push(gpn);
110                    }
111                } else {
112                    // Previous region ended exactly at a page boundary,
113                    // so first_gpn is the next page.
114                    for gpn in first_gpn..=last_gpn {
115                        gpns.push(gpn);
116                    }
117                }
118            } else {
119                // Not GPA-contiguous. Both the previous end and this
120                // start must be page-aligned to avoid a gap or overlap
121                // within a page slot.
122                if pe % PAGE_SIZE != 0 || addr % PAGE_SIZE != 0 {
123                    return None;
124                }
125                for gpn in first_gpn..=last_gpn {
126                    gpns.push(gpn);
127                }
128            }
129        } else {
130            // First region.
131            first_offset = Some((addr % PAGE_SIZE) as usize);
132            for gpn in first_gpn..=last_gpn {
133                gpns.push(gpn);
134            }
135        }
136
137        prev_end = Some(end);
138        total_len += len;
139    }
140
141    let offset = first_offset.unwrap_or(0);
142    Some((gpns, offset, total_len as usize))
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::queue::VirtioQueuePayload;
149    use guestmem::ranges::PagedRange;
150
151    fn payload(writeable: bool, address: u64, length: u32) -> VirtioQueuePayload {
152        VirtioQueuePayload {
153            writeable,
154            address,
155            length,
156        }
157    }
158
159    // ---- data_regions tests ----
160
161    #[test]
162    fn data_regions_read_single_descriptor() {
163        // Read: writable descriptors carry data, skip=0, exclude 1 byte for status.
164        let payloads = vec![payload(true, 0x1000, 4097)];
165        let regions: Vec<_> = data_regions(&payloads, true, 0, 4096).collect();
166        assert_eq!(regions.len(), 1);
167        assert_eq!(regions[0].addr, 0x1000);
168        assert_eq!(regions[0].len, 4096);
169    }
170
171    #[test]
172    fn data_regions_write_skips_header() {
173        // Write: readable descriptors carry data, skip header (16 bytes).
174        let payloads = vec![
175            payload(false, 0x1000, 16),  // header
176            payload(false, 0x2000, 512), // data
177        ];
178        let regions: Vec<_> = data_regions(&payloads, false, 16, 512).collect();
179        assert_eq!(regions.len(), 1);
180        assert_eq!(regions[0].addr, 0x2000);
181        assert_eq!(regions[0].len, 512);
182    }
183
184    #[test]
185    fn data_regions_write_header_spans_descriptors() {
186        // Header split across two descriptors.
187        let payloads = vec![
188            payload(false, 0x1000, 8),   // first 8 bytes of header
189            payload(false, 0x2000, 520), // remaining 8 header + 512 data
190        ];
191        let regions: Vec<_> = data_regions(&payloads, false, 16, 512).collect();
192        assert_eq!(regions.len(), 1);
193        assert_eq!(regions[0].addr, 0x2008); // 0x2000 + 8 skipped
194        assert_eq!(regions[0].len, 512);
195    }
196
197    #[test]
198    fn data_regions_filters_by_direction() {
199        // Readable and writable descriptors interleaved.
200        let payloads = vec![
201            payload(false, 0x1000, 16),  // readable: header
202            payload(true, 0x3000, 4097), // writable: data + status
203        ];
204        // Extract writable regions (read path).
205        let regions: Vec<_> = data_regions(&payloads, true, 0, 4096).collect();
206        assert_eq!(regions.len(), 1);
207        assert_eq!(regions[0].addr, 0x3000);
208        assert_eq!(regions[0].len, 4096);
209    }
210
211    #[test]
212    fn data_regions_empty_payload() {
213        let payloads: Vec<VirtioQueuePayload> = vec![];
214        let regions: Vec<_> = data_regions(&payloads, true, 0, 4096).collect();
215        assert!(regions.is_empty());
216    }
217
218    // ---- try_build_gpn_list tests ----
219
220    #[test]
221    fn gpn_list_single_page_aligned_region() {
222        let regions = vec![DataRegion {
223            addr: 0x1000,
224            len: 4096,
225        }];
226        let (gpns, offset, len) = try_build_gpn_list(&regions).unwrap();
227        assert_eq!(gpns, vec![1]); // GPN 1 = addr 0x1000
228        assert_eq!(offset, 0);
229        assert_eq!(len, 4096);
230    }
231
232    #[test]
233    fn gpn_list_single_region_with_offset() {
234        let regions = vec![DataRegion {
235            addr: 0x1200,
236            len: 512,
237        }];
238        let (gpns, offset, len) = try_build_gpn_list(&regions).unwrap();
239        assert_eq!(gpns, vec![1]);
240        assert_eq!(offset, 0x200);
241        assert_eq!(len, 512);
242    }
243
244    #[test]
245    fn gpn_list_single_region_spanning_pages() {
246        // 8192 bytes starting at page boundary → 2 pages.
247        let regions = vec![DataRegion {
248            addr: 0x2000,
249            len: 8192,
250        }];
251        let (gpns, offset, len) = try_build_gpn_list(&regions).unwrap();
252        assert_eq!(gpns, vec![2, 3]);
253        assert_eq!(offset, 0);
254        assert_eq!(len, 8192);
255    }
256
257    #[test]
258    fn gpn_list_two_page_aligned_non_contiguous_regions() {
259        // Two regions on different pages, both page-aligned boundaries.
260        let regions = vec![
261            DataRegion {
262                addr: 0x1000,
263                len: 4096,
264            },
265            DataRegion {
266                addr: 0x5000,
267                len: 4096,
268            },
269        ];
270        let (gpns, offset, len) = try_build_gpn_list(&regions).unwrap();
271        assert_eq!(gpns, vec![1, 5]);
272        assert_eq!(offset, 0);
273        assert_eq!(len, 8192);
274    }
275
276    #[test]
277    fn gpn_list_two_gpa_contiguous_regions() {
278        // Two regions that are GPA-contiguous (end of first == start of second).
279        let regions = vec![
280            DataRegion {
281                addr: 0x1000,
282                len: 4096,
283            },
284            DataRegion {
285                addr: 0x2000,
286                len: 4096,
287            },
288        ];
289        let (gpns, offset, len) = try_build_gpn_list(&regions).unwrap();
290        assert_eq!(gpns, vec![1, 2]);
291        assert_eq!(offset, 0);
292        assert_eq!(len, 8192);
293    }
294
295    #[test]
296    fn gpn_list_contiguous_mid_page_boundary() {
297        // Two GPA-contiguous regions sharing a page in the middle.
298        let regions = vec![
299            DataRegion {
300                addr: 0x1000,
301                len: 4608,
302            }, // ends at 0x2200
303            DataRegion {
304                addr: 0x2200,
305                len: 3584,
306            }, // starts at 0x2200, ends at 0x3000
307        ];
308        let (gpns, offset, len) = try_build_gpn_list(&regions).unwrap();
309        assert_eq!(gpns, vec![1, 2]);
310        assert_eq!(offset, 0);
311        assert_eq!(len, 8192);
312    }
313
314    #[test]
315    fn gpn_list_non_contiguous_non_aligned_fails() {
316        // Two non-contiguous regions where the boundary isn't page-aligned.
317        let regions = vec![
318            DataRegion {
319                addr: 0x1000,
320                len: 4608,
321            }, // ends at 0x2200, not page-aligned
322            DataRegion {
323                addr: 0x5200,
324                len: 512,
325            }, // different location, not page-aligned start
326        ];
327        assert!(try_build_gpn_list(&regions).is_none());
328    }
329
330    #[test]
331    fn gpn_list_non_contiguous_first_aligned_second_not() {
332        // First ends page-aligned, but second starts mid-page.
333        let regions = vec![
334            DataRegion {
335                addr: 0x1000,
336                len: 4096,
337            }, // ends at 0x2000 (aligned)
338            DataRegion {
339                addr: 0x5200,
340                len: 512,
341            }, // starts at 0x5200 (not aligned)
342        ];
343        assert!(try_build_gpn_list(&regions).is_none());
344    }
345
346    #[test]
347    fn gpn_list_non_contiguous_first_not_aligned_second_aligned() {
348        // First ends mid-page, second starts page-aligned.
349        let regions = vec![
350            DataRegion {
351                addr: 0x1000,
352                len: 4608,
353            }, // ends at 0x2200 (not aligned)
354            DataRegion {
355                addr: 0x5000,
356                len: 4096,
357            }, // starts page-aligned
358        ];
359        assert!(try_build_gpn_list(&regions).is_none());
360    }
361
362    #[test]
363    fn gpn_list_empty_regions() {
364        let regions: Vec<DataRegion> = vec![];
365        let (gpns, offset, len) = try_build_gpn_list(&regions).unwrap();
366        assert!(gpns.is_empty());
367        assert_eq!(offset, 0);
368        assert_eq!(len, 0);
369    }
370
371    #[test]
372    fn gpn_list_region_wrapping_64_bits_is_rejected() {
373        let regions = vec![DataRegion {
374            addr: 0xffff_ffff_ffff_ff00,
375            len: 512,
376        }];
377        assert!(try_build_gpn_list(&regions).is_none());
378    }
379
380    #[test]
381    fn gpn_list_chain_after_wrapping_region_is_rejected() {
382        let regions = vec![
383            DataRegion {
384                addr: 0xffff_ffff_ffff_ff00,
385                len: 512,
386            },
387            DataRegion {
388                addr: 0x100,
389                len: 512,
390            },
391        ];
392        assert!(try_build_gpn_list(&regions).is_none());
393    }
394
395    #[test]
396    fn gpn_list_three_page_aligned_regions() {
397        // Three separate page-aligned regions.
398        let regions = vec![
399            DataRegion {
400                addr: 0x1000,
401                len: 4096,
402            },
403            DataRegion {
404                addr: 0x3000,
405                len: 4096,
406            },
407            DataRegion {
408                addr: 0x7000,
409                len: 4096,
410            },
411        ];
412        let (gpns, offset, len) = try_build_gpn_list(&regions).unwrap();
413        assert_eq!(gpns, vec![1, 3, 7]);
414        assert_eq!(offset, 0);
415        assert_eq!(len, 12288);
416    }
417
418    #[test]
419    fn gpn_list_first_region_with_offset_second_page_aligned() {
420        // First region starts mid-page but ends at page boundary,
421        // second region starts at a different page boundary.
422        let regions = vec![
423            DataRegion {
424                addr: 0x1800,
425                len: 2048,
426            }, // 0x1800..0x2000
427            DataRegion {
428                addr: 0x5000,
429                len: 4096,
430            }, // 0x5000..0x6000
431        ];
432        let (gpns, offset, len) = try_build_gpn_list(&regions).unwrap();
433        assert_eq!(gpns, vec![1, 5]);
434        assert_eq!(offset, 0x800);
435        assert_eq!(len, 6144);
436    }
437
438    #[test]
439    fn gpn_list_validates_paged_range_construction() {
440        // Verify that the returned values actually produce a valid PagedRange.
441        let regions = vec![
442            DataRegion {
443                addr: 0x1000,
444                len: 4096,
445            },
446            DataRegion {
447                addr: 0x5000,
448                len: 8192,
449            },
450        ];
451        let (gpns, offset, len) = try_build_gpn_list(&regions).unwrap();
452        let range = PagedRange::new(offset, len, &gpns);
453        assert!(range.is_some());
454        assert_eq!(range.unwrap().len(), 12288);
455    }
456}