Skip to main content

imago/qcow2/
mappings.rs

1//! Get and establish cluster mappings.
2
3use super::*;
4use crate::sync_primitives::RwLockWriteGuard;
5
6#[maybe_async]
7impl<S: Storage, F: WrappedFormat<S>> Qcow2<S, F> {
8    /// Get the given range’s mapping information.
9    ///
10    /// Underlying implementation for [`Qcow2::get_mapping()`].
11    pub(super) async fn do_get_mapping(
12        &self,
13        offset: GuestOffset,
14        max_length: u64,
15    ) -> io::Result<(ShallowMapping<'_, S>, u64)> {
16        let Some(l2_table) = self.get_l2(offset, false).await? else {
17            let cb = self.header.cluster_bits();
18            let len = cmp::min(offset.remaining_in_l2_table(cb), max_length);
19            let mapping = if let Some(backing) = self.backing.as_ref() {
20                ShallowMapping::Indirect {
21                    layer: backing.inner(),
22                    offset: offset.0,
23                    writable: false,
24                }
25            } else {
26                ShallowMapping::Zero { explicit: false }
27            };
28            return Ok((mapping, len));
29        };
30
31        self.do_get_mapping_with_l2(offset, max_length, &l2_table)
32            .await
33    }
34
35    /// Get the given range’s mapping information, when we already have the L2 table.
36    pub(super) async fn do_get_mapping_with_l2(
37        &self,
38        offset: GuestOffset,
39        max_length: u64,
40        l2_table: &L2Table,
41    ) -> io::Result<(ShallowMapping<'_, S>, u64)> {
42        let cb = self.header.cluster_bits();
43
44        // Get mapping at `offset`
45        let mut current_guest_cluster = offset.cluster(cb);
46        let first_mapping = l2_table.get_mapping(current_guest_cluster)?;
47        let return_mapping = match first_mapping {
48            L2Mapping::DataFile {
49                host_cluster,
50                copied,
51            } => ShallowMapping::Raw {
52                storage: self.storage(),
53                offset: host_cluster.relative_offset(offset, cb).0,
54                writable: copied,
55            },
56
57            L2Mapping::Backing { backing_offset } => {
58                if let Some(backing) = self.backing.as_ref() {
59                    ShallowMapping::Indirect {
60                        layer: backing.inner(),
61                        offset: backing_offset + offset.in_cluster_offset(cb) as u64,
62                        writable: false,
63                    }
64                } else {
65                    ShallowMapping::Zero { explicit: false }
66                }
67            }
68
69            L2Mapping::Zero {
70                host_cluster: _,
71                copied: _,
72            } => ShallowMapping::Zero { explicit: true },
73
74            L2Mapping::Compressed {
75                host_offset: _,
76                length: _,
77            } => ShallowMapping::Special { offset: offset.0 },
78        };
79
80        // Find out how long this consecutive mapping is, but only within the current L2 table
81        let mut consecutive_length = offset.remaining_in_cluster(cb);
82        let mut preceding_mapping = first_mapping;
83        while consecutive_length < max_length {
84            let Some(next) = current_guest_cluster.next_in_l2(cb) else {
85                break;
86            };
87            current_guest_cluster = next;
88
89            let mapping = l2_table.get_mapping(current_guest_cluster)?;
90            if !mapping.is_consecutive(&preceding_mapping, cb) {
91                break;
92            }
93
94            preceding_mapping = mapping;
95            consecutive_length += self.header.cluster_size() as u64;
96        }
97
98        consecutive_length = cmp::min(consecutive_length, max_length);
99        Ok((return_mapping, consecutive_length))
100    }
101
102    /// Make the given range be mapped by data clusters.
103    ///
104    /// Underlying implementation for [`Qcow2::ensure_data_mapping()`].
105    ///
106    /// `skip_cow` is equivalent to [`Qcow2::ensure_data_mapping()`]’s `overwrite`: It indicates
107    /// the area is to be overwritten, so COW can be skipped on it.  `skip_cow_to_eof` indicates
108    /// that the mapping will go until the EOF, so no COW needs to be performed at all past
109    /// `offset`.  Only use this for preallocation on resize or create.
110    pub(super) async fn do_ensure_data_mapping(
111        &self,
112        offset: GuestOffset,
113        length: u64,
114        skip_cow: bool,
115        skip_cow_to_eof: bool,
116    ) -> io::Result<(&S, u64, u64)> {
117        let l2_table = self.ensure_l2(offset).await?;
118
119        // Fast path for if everything is already allocated, which should be the common case at
120        // runtime.
121        // It must really be everything, though; we know our caller will want to have everything
122        // allocated eventually, so if anything is missing, go down to the allocation path so we
123        // try to allocate clusters such that they are not fragmented (if possible) and we can
124        // return as big of a single mapping as possible.
125        let existing = self
126            .do_get_mapping_with_l2(offset, length, &l2_table)
127            .await?;
128        if let ShallowMapping::Raw {
129            storage,
130            offset,
131            writable: true,
132        } = existing.0
133        {
134            if existing.1 >= length {
135                return Ok((storage, offset, existing.1));
136            }
137        }
138
139        let l2_table = l2_table.lock_write().await;
140        let mut leaked_allocations = Vec::<(HostCluster, ClusterCount)>::new();
141
142        let res = self
143            .ensure_data_mapping_no_cleanup(
144                offset,
145                length,
146                skip_cow,
147                skip_cow_to_eof,
148                l2_table,
149                &mut leaked_allocations,
150            )
151            .await;
152
153        for alloc in leaked_allocations {
154            self.free_data_clusters(alloc.0, alloc.1).await;
155        }
156        let (host_offset, length) = res?;
157
158        Ok((self.storage(), host_offset, length))
159    }
160
161    /// Make the given range be mapped by a fixed kind of clusters.
162    ///
163    /// Allows zeroing or discarding clusters.  `mapping` says which kind of mapping to create.
164    ///
165    /// Return the offset of the first affected cluster, and the byte length affected (may be 0).
166    pub(super) async fn ensure_fixed_mapping(
167        &self,
168        offset: GuestOffset,
169        length: u64,
170        mapping: FixedMapping,
171    ) -> io::Result<(GuestOffset, u64)> {
172        match mapping {
173            FixedMapping::ZeroDiscard | FixedMapping::ZeroRetainAllocation => {
174                self.header.require_version(3)?;
175            }
176            FixedMapping::FullDiscard => (),
177        }
178
179        let cb = self.header.cluster_bits();
180
181        // We can only touch full clusters
182        let cluster_align_mask = self.header.cluster_size() as u64 - 1;
183        let end = (offset + length).0;
184        let aligned_end = if end == self.header.size() {
185            // Up-align operations until the image end to a full cluster (the remainder of this
186            // cluster is not used for anything)
187            (end + cluster_align_mask) & !cluster_align_mask
188        } else {
189            // Otherwise, align down (only full clusters)
190            end & !cluster_align_mask
191        };
192        let aligned_offset = (offset + cluster_align_mask).0 & !cluster_align_mask;
193        let aligned_length = aligned_end.saturating_sub(aligned_offset);
194
195        // We have aligned this, so we can unwrap
196        let first_cluster = GuestOffset(aligned_offset).checked_cluster(cb).unwrap();
197        let cluster_count = ClusterCount::checked_from_byte_size(aligned_length, cb).unwrap();
198
199        if cluster_count.0 == 0 {
200            return Ok((GuestOffset(aligned_offset), 0));
201        }
202
203        let l2_table = self.ensure_l2(first_cluster.offset(cb)).await?;
204        let l2_table = l2_table.lock_write().await;
205        let mut leaked_allocations = Vec::<(HostCluster, ClusterCount)>::new();
206
207        let res = self
208            .ensure_fixed_mapping_no_cleanup(
209                first_cluster,
210                cluster_count,
211                mapping,
212                l2_table,
213                &mut leaked_allocations,
214            )
215            .await;
216
217        for alloc in leaked_allocations {
218            self.free_data_clusters(alloc.0, alloc.1).await;
219        }
220
221        let count = res?;
222
223        let affected_offset = first_cluster.offset(cb);
224        let affected_length = count.byte_size(cb);
225
226        let head = affected_offset - offset;
227        // We may overshoot for the last cluster in the image, limit the returned value to the
228        // range given by the caller
229        let affected_length = cmp::min(affected_length, length.saturating_sub(head));
230
231        Ok((affected_offset, affected_length))
232    }
233
234    /// Get the L2 table referenced by the given L1 table index, if any.
235    ///
236    /// `writable` says whether the L2 table should be modifiable.
237    ///
238    /// If the L1 table index does not point to any L2 table, or the existing entry is not
239    /// modifiable but `writable` is true, return `Ok(None)`.
240    pub(super) async fn get_l2(
241        &self,
242        offset: GuestOffset,
243        writable: bool,
244    ) -> io::Result<Option<Arc<L2Table>>> {
245        let cb = self.header.cluster_bits();
246
247        let l1_entry = self.l1_table.read().await.get(offset.l1_index(cb));
248        if let Some(l2_offset) = l1_entry.l2_offset() {
249            if writable && !l1_entry.is_copied() {
250                return Ok(None);
251            }
252            let l2_cluster = l2_offset.checked_cluster(cb).ok_or_else(|| {
253                invalid_data(format!(
254                    "Unaligned L2 table for {offset:?}; L1 entry: {l1_entry:?}"
255                ))
256            })?;
257
258            self.caches.l2_get_or_insert(l2_cluster).await.map(Some)
259        } else {
260            Ok(None)
261        }
262    }
263
264    /// Get a L2 table for the given L1 table index.
265    ///
266    /// If there already is an L2 table at that index, return it.  Otherwise, create one and hook
267    /// it up.
268    pub(super) async fn ensure_l2(&self, offset: GuestOffset) -> io::Result<Arc<L2Table>> {
269        let cb = self.header.cluster_bits();
270
271        if let Some(l2) = self.get_l2(offset, true).await? {
272            return Ok(l2);
273        }
274
275        self.need_writable()?;
276
277        let mut l1_locked = self.l1_table.write().await;
278        let l1_index = offset.l1_index(cb);
279        if !l1_locked.in_bounds(l1_index) {
280            l1_locked = self.grow_l1_table(l1_locked, l1_index).await?;
281        }
282
283        let l1_entry = l1_locked.get(l1_index);
284        let mut l2_table = if let Some(l2_offset) = l1_entry.l2_offset() {
285            let l2_cluster = l2_offset.checked_cluster(cb).ok_or_else(|| {
286                invalid_data(format!(
287                    "Unaligned L2 table for {offset:?}; L1 entry: {l1_entry:?}"
288                ))
289            })?;
290
291            let l2 = self.caches.l2_get_or_insert(l2_cluster).await?;
292            if l1_entry.is_copied() {
293                return Ok(l2);
294            }
295
296            L2Table::clone(&l2)
297        } else {
298            L2Table::new_cleared(&self.header)
299        };
300
301        let l2_cluster = self.allocate_meta_cluster().await?;
302        l2_table.set_cluster(l2_cluster);
303        l2_table.write(self.metadata.as_ref()).await?;
304
305        l1_locked.enter_l2_table(l1_index, &l2_table)?;
306        l1_locked
307            .write_entry(self.metadata.as_ref(), l1_index)
308            .await?;
309
310        // Free old L2 table, if any
311        if let Some(l2_offset) = l1_entry.l2_offset() {
312            self.free_meta_clusters(l2_offset.cluster(cb), ClusterCount(1))
313                .await;
314        }
315
316        let l2_table = Arc::new(l2_table);
317        self.caches
318            .l2_insert(l2_cluster, Arc::clone(&l2_table))
319            .await?;
320        Ok(l2_table)
321    }
322
323    /// Create a new L1 table covering at least `at_least_index`.
324    ///
325    /// Create a new L1 table of the required size with all the entries of the previous L1 table.
326    pub(super) async fn grow_l1_table<'a>(
327        &self,
328        mut l1_locked: RwLockWriteGuard<'a, L1Table>,
329        at_least_index: usize,
330    ) -> io::Result<RwLockWriteGuard<'a, L1Table>> {
331        let mut new_l1 = l1_locked.clone_and_grow(at_least_index, &self.header)?;
332
333        let l1_start = self.allocate_meta_clusters(new_l1.cluster_count()).await?;
334
335        new_l1.set_cluster(l1_start);
336        new_l1.write(self.metadata.as_ref()).await?;
337
338        self.header.set_l1_table(&new_l1)?;
339        self.header
340            .write_l1_table_pointer(self.metadata.as_ref())
341            .await?;
342
343        if let Some(old_l1_cluster) = l1_locked.get_cluster() {
344            let old_l1_size = l1_locked.cluster_count();
345            l1_locked.unset_cluster();
346            self.free_meta_clusters(old_l1_cluster, old_l1_size).await;
347        }
348
349        *l1_locked = new_l1;
350
351        Ok(l1_locked)
352    }
353
354    /// Inner implementation for [`Qcow2::do_ensure_data_mapping()`].
355    ///
356    /// Does not do any clean-up: The L2 table will probably be modified, but not written to disk.
357    /// Any existing allocations that have been removed from it (and are thus leaked) are entered
358    /// into `leaked_allocations`, but not freed.
359    ///
360    /// The caller must do both, ensuring it is done both in case of success and in case of error.
361    async fn ensure_data_mapping_no_cleanup(
362        &self,
363        offset: GuestOffset,
364        full_length: u64,
365        skip_cow: bool,
366        skip_cow_to_eof: bool,
367        mut l2_table: L2TableWriteGuard<'_>,
368        leaked_allocations: &mut Vec<(HostCluster, ClusterCount)>,
369    ) -> io::Result<(u64, u64)> {
370        let cb = self.header.cluster_bits();
371
372        let partial_skip_cow = skip_cow.then(|| {
373            let start = offset.in_cluster_offset(cb);
374            let end = if skip_cow_to_eof {
375                1 << cb
376            } else {
377                cmp::min(start as u64 + full_length, 1 << cb) as usize
378            };
379            start..end
380        });
381
382        let mut current_guest_cluster = offset.cluster(cb);
383
384        // Without a mandatory host offset, this should never return `Ok(None)`
385        let host_cluster = self
386            .cow_cluster(
387                current_guest_cluster,
388                None,
389                partial_skip_cow,
390                &mut l2_table,
391                leaked_allocations,
392            )
393            .await?
394            .ok_or_else(|| io::Error::other("Internal allocation error"))?;
395
396        let host_offset_start = host_cluster.relative_offset(offset, cb);
397        let mut allocated_length = offset.remaining_in_cluster(cb);
398        let mut current_host_cluster = host_cluster;
399
400        while allocated_length < full_length {
401            let Some(next) = current_guest_cluster.next_in_l2(cb) else {
402                break;
403            };
404            current_guest_cluster = next;
405
406            let chunk_length = cmp::min(full_length - allocated_length, 1 << cb) as usize;
407            let partial_skip_cow = match (skip_cow, skip_cow_to_eof) {
408                (false, _) => None,
409                (true, false) => Some(0..chunk_length),
410                (true, true) => Some(0..(1 << cb)),
411            };
412
413            let next_host_cluster = current_host_cluster + ClusterCount(1);
414            let host_cluster = self
415                .cow_cluster(
416                    current_guest_cluster,
417                    Some(next_host_cluster),
418                    partial_skip_cow,
419                    &mut l2_table,
420                    leaked_allocations,
421                )
422                .await?;
423
424            let Some(host_cluster) = host_cluster else {
425                // Cannot continue continuous mapping range
426                break;
427            };
428            assert!(host_cluster == next_host_cluster);
429            current_host_cluster = host_cluster;
430
431            allocated_length += chunk_length as u64;
432        }
433
434        Ok((host_offset_start.0, allocated_length))
435    }
436
437    /// Inner implementation for [`Qcow2::ensure_fixed_mapping()`].
438    ///
439    /// Does not do any clean-up: The L2 table will probably be modified, but not written to disk.
440    /// Any existing allocations that have been removed from it (and are thus leaked) are entered
441    /// into `leaked_allocations`, but not freed.
442    ///
443    /// The caller must do both, ensuring it is done both in case of success and in case of error.
444    ///
445    /// Allows zeroing or discarding clusters.  `mapping` says which kind of mapping to create.
446    async fn ensure_fixed_mapping_no_cleanup(
447        &self,
448        first_cluster: GuestCluster,
449        count: ClusterCount,
450        mapping: FixedMapping,
451        mut l2_table: L2TableWriteGuard<'_>,
452        leaked_allocations: &mut Vec<(HostCluster, ClusterCount)>,
453    ) -> io::Result<ClusterCount> {
454        self.header.require_version(3)?;
455
456        let cb = self.header.cluster_bits();
457        let mut cluster = first_cluster;
458        let end_cluster = first_cluster + count;
459        let mut done = ClusterCount(0);
460
461        while cluster < end_cluster {
462            let l2i = cluster.l2_index(cb);
463            let leaked = match mapping {
464                FixedMapping::ZeroDiscard => l2_table.zero_cluster(l2i, false)?,
465                FixedMapping::ZeroRetainAllocation => l2_table.zero_cluster(l2i, true)?,
466                FixedMapping::FullDiscard => l2_table.discard_cluster(l2i),
467            };
468            if let Some(leaked) = leaked {
469                leaked_allocations.push(leaked);
470            }
471
472            done += ClusterCount(1);
473            let Some(next) = cluster.next_in_l2(cb) else {
474                break;
475            };
476            cluster = next;
477        }
478
479        Ok(done)
480    }
481}
482
483/// Possible mapping types for [`Qcow2::ensure_fixed_mapping()`].
484#[derive(Clone, Copy, Debug, Eq, PartialEq)]
485pub(super) enum FixedMapping {
486    /// Make all clusters zero clusters, discarding previous allocations.
487    ///
488    /// Note this breaks existing mapping information, which must be communicated somehow, for
489    /// example by requiring mutable access to the `Qcow2` object.
490    ZeroDiscard,
491
492    /// Make all clusters zero clusters, retaining previous allocations.
493    ///
494    /// Retains previous data cluster allocations in the form of preallocated zero clusters, but
495    /// cannot retain previously existing compressed cluster allocations.  Because those mappings
496    /// are not returned through the mapping interface, however, concurrent accesses should be
497    /// reasonably safe.
498    ///
499    /// (Writing to zeroed data cluster mappings will just have no effect.)
500    ZeroRetainAllocation,
501
502    /// Fully remove clusters’ mappings, allowing backing data to appear.
503    ///
504    /// Note this breaks existing mapping information, which must be communicated somehow, for
505    /// example by requiring mutable access to the `Qcow2` object.
506    FullDiscard,
507}