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    /// Prepare mapping the given range by data clusters.
103    ///
104    /// Underlying implementation for [`Qcow2::prepare_data_mapping()`].
105    ///
106    /// Return the host offset to write the data to, the length that could be mapped, and, unless
107    /// the whole returned range is mapped to that host offset already, the state to pass to
108    /// [`Qcow2::do_commit_data_mapping()`] to make the mapping visible.  That state keeps the L2
109    /// table locked for modifications; dropping it frees the clusters allocated for the mapping.
110    ///
111    /// `skip_cow` is equivalent to [`Qcow2::ensure_data_mapping()`]’s `overwrite`: It indicates
112    /// the area is to be overwritten, so COW can be skipped on it.  `skip_cow_to_eof` indicates
113    /// that the mapping will go until the EOF, so no COW needs to be performed at all past
114    /// `offset`.  Only use this for preallocation on resize or create.
115    pub(super) async fn do_prepare_data_mapping(
116        &self,
117        offset: GuestOffset,
118        length: u64,
119        skip_cow: bool,
120        skip_cow_to_eof: bool,
121    ) -> io::Result<(u64, u64, Option<PreparedMappingState>)> {
122        let l2_table = self.ensure_l2(offset).await?;
123
124        // Fast path for if everything is already allocated, which should be the common case at
125        // runtime.
126        // It must really be everything, though; we know our caller will want to have everything
127        // allocated eventually, so if anything is missing, go down to the allocation path so we
128        // try to allocate clusters such that they are not fragmented (if possible) and we can
129        // return as big of a single mapping as possible.
130        let existing = self
131            .do_get_mapping_with_l2(offset, length, &l2_table)
132            .await?;
133        if let ShallowMapping::Raw {
134            storage: _,
135            offset,
136            writable: true,
137        } = existing.0
138        {
139            if existing.1 >= length {
140                return Ok((offset, existing.1, None));
141            }
142        }
143
144        let l2_table = l2_table.lock_write().await;
145
146        let mut l2_changes = Vec::new();
147        let res = self
148            .prepare_data_mapping_no_cleanup(
149                offset,
150                length,
151                skip_cow,
152                skip_cow_to_eof,
153                &l2_table,
154                &mut l2_changes,
155            )
156            .await;
157
158        let (host_offset, length) = match res {
159            Ok(mapped) => mapped,
160            Err(err) => {
161                // Release what has been allocated so far
162                drop(l2_changes);
163                drop(l2_table);
164                self.process_clusters_to_free().await;
165                return Err(err);
166            }
167        };
168
169        // Everything may turn out to be mapped already, e.g. when a concurrent request has mapped
170        // it while we were waiting for the lock
171        let prepared_mapping = (!l2_changes.is_empty()).then(|| PreparedMappingState {
172            l2_table,
173            l2_changes,
174            clusters_to_free: Arc::clone(&self.clusters_to_free),
175        });
176
177        Ok((host_offset, length, prepared_mapping))
178    }
179
180    /// Make a mapping prepared by [`Qcow2::do_prepare_data_mapping()`] visible.
181    ///
182    /// Underlying implementation for [`Qcow2::commit_data_mapping()`].
183    pub(super) async fn do_commit_data_mapping(&self, state: PreparedMappingState) {
184        let PreparedMappingState {
185            mut l2_table,
186            l2_changes,
187            clusters_to_free: _,
188        } = state;
189
190        for (l2_index, host_cluster, new_allocation) in l2_changes {
191            if let Some(leaked) = l2_table.map_cluster(l2_index, host_cluster, new_allocation) {
192                self.clusters_to_free.push(leaked);
193            }
194        }
195        drop(l2_table);
196
197        self.process_clusters_to_free().await;
198    }
199
200    /// Make the given range be mapped by a fixed kind of clusters.
201    ///
202    /// Allows zeroing or discarding clusters.  `mapping` says which kind of mapping to create.
203    ///
204    /// Return the offset of the first affected cluster, and the byte length affected (may be 0).
205    pub(super) async fn ensure_fixed_mapping(
206        &self,
207        offset: GuestOffset,
208        length: u64,
209        mapping: FixedMapping,
210    ) -> io::Result<(GuestOffset, u64)> {
211        match mapping {
212            FixedMapping::ZeroDiscard | FixedMapping::ZeroRetainAllocation => {
213                self.header.require_version(3)?;
214            }
215            FixedMapping::FullDiscard => (),
216        }
217
218        let cb = self.header.cluster_bits();
219
220        // We can only touch full clusters
221        let cluster_align_mask = self.header.cluster_size() as u64 - 1;
222        let end = (offset + length).0;
223        let aligned_end = if end == self.header.size() {
224            // Up-align operations until the image end to a full cluster (the remainder of this
225            // cluster is not used for anything)
226            (end + cluster_align_mask) & !cluster_align_mask
227        } else {
228            // Otherwise, align down (only full clusters)
229            end & !cluster_align_mask
230        };
231        let aligned_offset = (offset + cluster_align_mask).0 & !cluster_align_mask;
232        let aligned_length = aligned_end.saturating_sub(aligned_offset);
233
234        // We have aligned this, so we can unwrap
235        let first_cluster = GuestOffset(aligned_offset).checked_cluster(cb).unwrap();
236        let cluster_count = ClusterCount::checked_from_byte_size(aligned_length, cb).unwrap();
237
238        if cluster_count.0 == 0 {
239            return Ok((GuestOffset(aligned_offset), 0));
240        }
241
242        let l2_table = self.ensure_l2(first_cluster.offset(cb)).await?;
243        let l2_table = l2_table.lock_write().await;
244
245        let res = self
246            .ensure_fixed_mapping_no_cleanup(first_cluster, cluster_count, mapping, l2_table)
247            .await;
248
249        self.process_clusters_to_free().await;
250
251        let count = res?;
252
253        let affected_offset = first_cluster.offset(cb);
254        let affected_length = count.byte_size(cb);
255
256        let head = affected_offset - offset;
257        // We may overshoot for the last cluster in the image, limit the returned value to the
258        // range given by the caller
259        let affected_length = cmp::min(affected_length, length.saturating_sub(head));
260
261        Ok((affected_offset, affected_length))
262    }
263
264    /// Get the L2 table referenced by the given L1 table index, if any.
265    ///
266    /// `writable` says whether the L2 table should be modifiable.
267    ///
268    /// If the L1 table index does not point to any L2 table, or the existing entry is not
269    /// modifiable but `writable` is true, return `Ok(None)`.
270    pub(super) async fn get_l2(
271        &self,
272        offset: GuestOffset,
273        writable: bool,
274    ) -> io::Result<Option<Arc<L2Table>>> {
275        let cb = self.header.cluster_bits();
276
277        let l1_entry = self.l1_table.read().await.get(offset.l1_index(cb));
278        if let Some(l2_offset) = l1_entry.l2_offset() {
279            if writable && !l1_entry.is_copied() {
280                return Ok(None);
281            }
282            let l2_cluster = l2_offset.checked_cluster(cb).ok_or_else(|| {
283                invalid_data(format!(
284                    "Unaligned L2 table for {offset:?}; L1 entry: {l1_entry:?}"
285                ))
286            })?;
287
288            self.caches.l2_get_or_insert(l2_cluster).await.map(Some)
289        } else {
290            Ok(None)
291        }
292    }
293
294    /// Get a L2 table for the given L1 table index.
295    ///
296    /// If there already is an L2 table at that index, return it.  Otherwise, create one and hook
297    /// it up.
298    pub(super) async fn ensure_l2(&self, offset: GuestOffset) -> io::Result<Arc<L2Table>> {
299        let cb = self.header.cluster_bits();
300
301        if let Some(l2) = self.get_l2(offset, true).await? {
302            return Ok(l2);
303        }
304
305        self.need_writable()?;
306
307        let mut l1_locked = self.l1_table.write().await;
308        let l1_index = offset.l1_index(cb);
309        if !l1_locked.in_bounds(l1_index) {
310            l1_locked = self.grow_l1_table(l1_locked, l1_index).await?;
311        }
312
313        let l1_entry = l1_locked.get(l1_index);
314        let mut l2_table = if let Some(l2_offset) = l1_entry.l2_offset() {
315            let l2_cluster = l2_offset.checked_cluster(cb).ok_or_else(|| {
316                invalid_data(format!(
317                    "Unaligned L2 table for {offset:?}; L1 entry: {l1_entry:?}"
318                ))
319            })?;
320
321            let l2 = self.caches.l2_get_or_insert(l2_cluster).await?;
322            if l1_entry.is_copied() {
323                return Ok(l2);
324            }
325
326            L2Table::clone(&l2)
327        } else {
328            L2Table::new_cleared(&self.header)
329        };
330
331        let l2_allocation = self.allocate_meta_cluster().await?;
332        l2_table.set_cluster(l2_allocation.cluster());
333        l2_table.write(self.metadata.as_ref()).await?;
334
335        l1_locked.enter_l2_table(l1_index, &l2_table, Some(l2_allocation))?;
336        l1_locked
337            .write_entry(self.metadata.as_ref(), l1_index)
338            .await?;
339
340        // Free old L2 table, if any
341        if let Some(l2_offset) = l1_entry.l2_offset() {
342            self.clusters_to_free.push(ClustersToFreeEntry::metadata(
343                l2_offset.cluster(cb),
344                ClusterCount(1),
345            ));
346            self.process_clusters_to_free().await;
347        }
348
349        let l2_table = Arc::new(l2_table);
350        self.caches.l2_insert(Arc::clone(&l2_table)).await?;
351        Ok(l2_table)
352    }
353
354    /// Create a new L1 table covering at least `at_least_index`.
355    ///
356    /// Create a new L1 table of the required size with all the entries of the previous L1 table.
357    pub(super) async fn grow_l1_table<'a>(
358        &self,
359        mut l1_locked: RwLockWriteGuard<'a, L1Table>,
360        at_least_index: usize,
361    ) -> io::Result<RwLockWriteGuard<'a, L1Table>> {
362        let mut new_l1 = l1_locked.clone_and_grow(at_least_index, &self.header)?;
363
364        let l1_allocation = self.allocate_meta_clusters(new_l1.cluster_count()).await?;
365
366        new_l1.set_cluster(l1_allocation.cluster());
367        new_l1.write(self.metadata.as_ref()).await?;
368
369        self.header.set_l1_table(&new_l1, Some(l1_allocation))?;
370        self.header
371            .write_l1_table_pointer(self.metadata.as_ref())
372            .await?;
373
374        if let Some(old_l1_cluster) = l1_locked.get_cluster() {
375            let old_l1_size = l1_locked.cluster_count();
376            l1_locked.unset_cluster();
377            self.clusters_to_free
378                .push(ClustersToFreeEntry::metadata(old_l1_cluster, old_l1_size));
379            self.process_clusters_to_free().await;
380        }
381
382        *l1_locked = new_l1;
383
384        Ok(l1_locked)
385    }
386
387    /// Inner implementation for [`Qcow2::do_prepare_data_mapping()`].
388    ///
389    /// Does not modify the L2 table: Whatever must be entered into it to make the mapping visible
390    /// is pushed to `l2_changes` (L2 index, host cluster, and the guard for the host cluster if it
391    /// has been newly allocated).
392    ///
393    /// Does not do any clean-up: In case of error, the caller must drop `l2_changes`, which enters
394    /// the clusters allocated so far into `self.clusters_to_free`, and free those.
395    async fn prepare_data_mapping_no_cleanup(
396        &self,
397        offset: GuestOffset,
398        full_length: u64,
399        skip_cow: bool,
400        skip_cow_to_eof: bool,
401        l2_table: &L2TableWriteGuard,
402        l2_changes: &mut Vec<(usize, HostCluster, Option<PendingClusters>)>,
403    ) -> io::Result<(u64, u64)> {
404        let cb = self.header.cluster_bits();
405
406        let partial_skip_cow = skip_cow.then(|| {
407            let start = offset.in_cluster_offset(cb);
408            let end = if skip_cow_to_eof {
409                1 << cb
410            } else {
411                cmp::min(start as u64 + full_length, 1 << cb) as usize
412            };
413            start..end
414        });
415
416        let mut current_guest_cluster = offset.cluster(cb);
417
418        // Without a mandatory host offset, this should never return `Ok(None)`
419        let prepared = self
420            .prepare_cow_cluster(current_guest_cluster, None, partial_skip_cow, l2_table)
421            .await?
422            .ok_or_else(|| io::Error::other("Internal allocation error"))?;
423        let host_cluster = prepared.host_cluster;
424        if let Some((l2_index, new_allocation)) = prepared.set_mapping {
425            l2_changes.push((l2_index, host_cluster, new_allocation));
426        }
427
428        let host_offset_start = host_cluster.relative_offset(offset, cb);
429        let mut allocated_length = offset.remaining_in_cluster(cb);
430        let mut current_host_cluster = host_cluster;
431
432        while allocated_length < full_length {
433            let Some(next) = current_guest_cluster.next_in_l2(cb) else {
434                break;
435            };
436            current_guest_cluster = next;
437
438            let chunk_length = cmp::min(full_length - allocated_length, 1 << cb) as usize;
439            let partial_skip_cow = match (skip_cow, skip_cow_to_eof) {
440                (false, _) => None,
441                (true, false) => Some(0..chunk_length),
442                (true, true) => Some(0..(1 << cb)),
443            };
444
445            let next_host_cluster = current_host_cluster + ClusterCount(1);
446            let prepared = self
447                .prepare_cow_cluster(
448                    current_guest_cluster,
449                    Some(next_host_cluster),
450                    partial_skip_cow,
451                    l2_table,
452                )
453                .await?;
454
455            let Some(prepared) = prepared else {
456                // Cannot continue continuous mapping range
457                break;
458            };
459            let host_cluster = prepared.host_cluster;
460            assert!(host_cluster == next_host_cluster);
461            if let Some((l2_index, new_allocation)) = prepared.set_mapping {
462                l2_changes.push((l2_index, host_cluster, new_allocation));
463            }
464            current_host_cluster = host_cluster;
465
466            allocated_length += chunk_length as u64;
467        }
468
469        Ok((host_offset_start.0, allocated_length))
470    }
471
472    /// Inner implementation for [`Qcow2::ensure_fixed_mapping()`].
473    ///
474    /// Does not do any clean-up: The L2 table will probably be modified, but not written to disk.
475    /// Any existing allocations that have been removed from it (and are thus leaked) are entered
476    /// into `self.clusters_to_free`, but not freed.
477    ///
478    /// The caller must do both, ensuring it is done both in case of success and in case of error.
479    ///
480    /// Allows zeroing or discarding clusters.  `mapping` says which kind of mapping to create.
481    async fn ensure_fixed_mapping_no_cleanup(
482        &self,
483        first_cluster: GuestCluster,
484        count: ClusterCount,
485        mapping: FixedMapping,
486        mut l2_table: L2TableWriteGuard,
487    ) -> io::Result<ClusterCount> {
488        let cb = self.header.cluster_bits();
489        let mut cluster = first_cluster;
490        let end_cluster = first_cluster + count;
491        let mut done = ClusterCount(0);
492
493        while cluster < end_cluster {
494            let l2i = cluster.l2_index(cb);
495            let leaked = match mapping {
496                FixedMapping::ZeroDiscard => l2_table.zero_cluster(l2i, false)?,
497                FixedMapping::ZeroRetainAllocation => l2_table.zero_cluster(l2i, true)?,
498                FixedMapping::FullDiscard => l2_table.discard_cluster(l2i),
499            };
500            if let Some(leaked) = leaked {
501                self.clusters_to_free.push(leaked);
502            }
503
504            done += ClusterCount(1);
505            let Some(next) = cluster.next_in_l2(cb) else {
506                break;
507            };
508            cluster = next;
509        }
510
511        Ok(done)
512    }
513}
514
515/// State of a data mapping that has been prepared, but not committed yet.
516///
517/// Created by [`Qcow2::do_prepare_data_mapping()`], to be passed to
518/// [`Qcow2::do_commit_data_mapping()`].
519///
520/// Dropping this discards the prepared mapping, i.e. enters the clusters that have been allocated
521/// for it into the list of clusters to free.
522pub(super) struct PreparedMappingState {
523    /// Write guard on the L2 table that is to be modified.
524    ///
525    /// Held so that nobody else modifies the affected L2 entries before the mapping is committed
526    /// or dropped; in particular, nobody else may prepare a mapping for the same guest clusters.
527    ///
528    /// This also occupies an L2 cache entry for as long as it exists, because the cache only
529    /// evicts tables that nothing else refers to.  Prepared mappings are held across data I/O, so
530    /// holding several of them can pin the whole L2 cache: If no entry can be evicted, loading
531    /// another L2 table fails with an I/O error (“Cannot evict entry from cache; everything is in
532    /// use”) instead of waiting for an entry to become available.
533    l2_table: L2TableWriteGuard,
534
535    /// What to enter into the L2 table: L2 index, host cluster, and the guard for the host cluster
536    /// if it has been newly allocated.
537    l2_changes: Vec<(usize, HostCluster, Option<PendingClusters>)>,
538
539    /// The image’s list of clusters to free, to identify which image this state belongs to.
540    clusters_to_free: Arc<ClustersToFreeList>,
541}
542
543impl PreparedMappingState {
544    /// Check whether this state belongs to the given qcow2 image instance
545    pub fn belongs_to<S: Storage + 'static, F: WrappedFormat<S> + 'static>(
546        &self,
547        qcow2: &Qcow2<S, F>,
548    ) -> bool {
549        Arc::ptr_eq(&self.clusters_to_free, &qcow2.clusters_to_free)
550    }
551}
552
553/// Possible mapping types for [`Qcow2::ensure_fixed_mapping()`].
554#[derive(Clone, Copy, Debug, Eq, PartialEq)]
555pub(super) enum FixedMapping {
556    /// Make all clusters zero clusters, discarding previous allocations.
557    ///
558    /// Note this breaks existing mapping information, which must be communicated somehow, for
559    /// example by requiring mutable access to the `Qcow2` object.
560    ZeroDiscard,
561
562    /// Make all clusters zero clusters, retaining previous allocations.
563    ///
564    /// Retains previous data cluster allocations in the form of preallocated zero clusters, but
565    /// cannot retain previously existing compressed cluster allocations.  Because those mappings
566    /// are not returned through the mapping interface, however, concurrent accesses should be
567    /// reasonably safe.
568    ///
569    /// (Writing to zeroed data cluster mappings will just have no effect.)
570    ZeroRetainAllocation,
571
572    /// Fully remove clusters’ mappings, allowing backing data to appear.
573    ///
574    /// Note this breaks existing mapping information, which must be communicated somehow, for
575    /// example by requiring mutable access to the `Qcow2` object.
576    FullDiscard,
577}
578
579#[cfg(test)]
580mod tests {
581    //! Tests for establishing mappings.
582
583    use super::*;
584    use crate::format::drivers::FormatDriverInstance;
585    use crate::test::mem_storage::{MemStorage, MemStorageState};
586    use crate::{DenyImplicitOpenGate, Mapping};
587
588    /// Create a 512-byte-cluster image of the given size and refcount width on a [`MemStorage`]
589    /// with the given state.
590    #[maybe_async]
591    async fn create_image(
592        state: &Arc<MemStorageState>,
593        size: u64,
594        refcount_width: usize,
595    ) -> Qcow2<MemStorage> {
596        Qcow2::<MemStorage>::create_builder(MemStorage::new(Arc::clone(state), false))
597            .cluster_size(512)
598            .refcount_width(refcount_width)
599            .size(size)
600            .create_open(DenyImplicitOpenGate::default(), |image| {
601                Ok(Qcow2::<MemStorage>::builder(image)
602                    .backing(None)
603                    .write(true))
604            })
605            .await
606            .unwrap()
607    }
608
609    /// Load the reftable from storage.
610    #[cfg(feature = "async")]
611    #[maybe_async]
612    async fn load_reftable(qcow2: &Qcow2<MemStorage>) -> RefTable {
613        let cb = qcow2.header.cluster_bits();
614        let rt_cluster = qcow2.header.reftable_offset().checked_cluster(cb).unwrap();
615        RefTable::load(
616            qcow2.metadata.as_ref(),
617            &qcow2.header,
618            rt_cluster,
619            qcow2.header.reftable_entries(),
620        )
621        .await
622        .unwrap()
623    }
624
625    /// Two concurrent flushes writing back the same L2 table, with the table modified in between:
626    /// The modification must end up on disk, i.e. the older copy must not be written last.
627    #[cfg(feature = "async")]
628    #[tokio::test]
629    async fn test_concurrent_l2_writeback() {
630        use futures::task::noop_waker;
631        use std::future::Future;
632        use std::sync::atomic::Ordering;
633        use std::task::{Context, Poll};
634
635        let state = Arc::new(MemStorageState::default());
636        let qcow2 = create_image(&state, 64 * 1024, 16).await;
637        let cb = qcow2.header.cluster_bits();
638
639        // Map guest clusters 0 and 1 (all in the same L2 table), and write everything
640        qcow2.ensure_data_mapping(0, 1024, true).await.unwrap();
641        qcow2.flush().await.unwrap();
642
643        // Modify the L2 table, so the next flush will write it
644        qcow2.ensure_data_mapping(1024, 512, true).await.unwrap();
645
646        let l2_cluster = {
647            let l1 = qcow2.l1_table.read().await;
648            l1.get(GuestOffset(0).l1_index(cb))
649                .l2_offset()
650                .unwrap()
651                .checked_cluster(cb)
652                .unwrap()
653        };
654        let l2_offset = l2_cluster.offset(cb).0;
655
656        let waker = noop_waker();
657        let mut cx = Context::from_waker(&waker);
658
659        // Flush A: Keep it pending on writing the L2 table, after it has copied its entries
660        *state.hold_range.lock() = Some(l2_offset..l2_offset + 512);
661        state.hold_writes.store(true, Ordering::Relaxed);
662        let mut flush_a = Box::pin(qcow2.flush());
663        assert!(flush_a.as_mut().poll(&mut cx).is_pending());
664        assert_eq!(state.held.lock().len(), 1);
665        state.hold_writes.store(false, Ordering::Relaxed);
666
667        // Modify the L2 table again: Make guest cluster 1 a zero cluster
668        qcow2.ensure_zero_mapping(512, 512).await.unwrap();
669
670        // Flush B: Must write the L2 table after flush A
671        let mut flush_b = Box::pin(qcow2.flush());
672        let flush_b_result = match flush_b.as_mut().poll(&mut cx) {
673            Poll::Ready(result) => Some(result),
674            Poll::Pending => None,
675        };
676
677        state.release_held_writes();
678        let Poll::Ready(result) = flush_a.as_mut().poll(&mut cx) else {
679            panic!("Flush A did not complete");
680        };
681        result.unwrap();
682        match flush_b_result {
683            Some(result) => result.unwrap(),
684            None => flush_b.await.unwrap(),
685        }
686
687        // Guest cluster 1’s L2 entry must have the zero flag (bit 0) on disk
688        let entry_offset = (l2_offset + 8) as usize;
689        let data = state.data.lock();
690        let entry = u64::from_be_bytes(data[entry_offset..entry_offset + 8].try_into().unwrap());
691        assert!(
692            entry & 1 != 0,
693            "zeroing guest cluster 1 lost on disk (L2 entry {entry:#x})"
694        );
695    }
696
697    /// Two concurrent flushes writing back the same refblock, with a refcount changed in between:
698    /// The change must end up on disk, i.e. the older copy must not be written last.
699    #[cfg(feature = "async")]
700    #[tokio::test]
701    async fn test_concurrent_refblock_writeback() {
702        use futures::task::noop_waker;
703        use std::future::Future;
704        use std::sync::atomic::Ordering;
705        use std::task::{Context, Poll};
706
707        let state = Arc::new(MemStorageState::default());
708        let qcow2 = create_image(&state, 64 * 1024, 16).await;
709        let rb_bits = qcow2.header.rb_bits();
710        let refcount_bytes = 2; // 16-bit refcounts
711        qcow2.flush().await.unwrap();
712
713        // Modify the refblock, so the next flush will write it
714        let first_pending = qcow2.allocate_meta_cluster().await.unwrap();
715        let first = first_pending.cluster();
716        first_pending.make_persistent();
717        assert_eq!(first.rt_index(rb_bits), 0);
718        let rb_offset = load_reftable(&qcow2)
719            .await
720            .get(0)
721            .refblock_offset()
722            .unwrap()
723            .0;
724
725        let waker = noop_waker();
726        let mut cx = Context::from_waker(&waker);
727
728        // Flush A: Keep it pending on writing the refblock, after it has copied its data
729        *state.hold_range.lock() = Some(rb_offset..rb_offset + 512);
730        state.hold_writes.store(true, Ordering::Relaxed);
731        let mut flush_a = Box::pin(qcow2.flush());
732        assert!(flush_a.as_mut().poll(&mut cx).is_pending());
733        assert_eq!(state.held.lock().len(), 1);
734        state.hold_writes.store(false, Ordering::Relaxed);
735
736        // Modify the refblock again
737        let second_pending = qcow2.allocate_meta_cluster().await.unwrap();
738        let second = second_pending.cluster();
739        second_pending.make_persistent();
740        assert_eq!(second.rt_index(rb_bits), 0);
741
742        // Flush B: Must write the refblock after flush A
743        let mut flush_b = Box::pin(qcow2.flush());
744        let flush_b_result = match flush_b.as_mut().poll(&mut cx) {
745            Poll::Ready(result) => Some(result),
746            Poll::Pending => None,
747        };
748
749        state.release_held_writes();
750        let Poll::Ready(result) = flush_a.as_mut().poll(&mut cx) else {
751            panic!("Flush A did not complete");
752        };
753        result.unwrap();
754        match flush_b_result {
755            Some(result) => result.unwrap(),
756            None => flush_b.await.unwrap(),
757        }
758
759        // The second cluster’s refcount must be 1 on disk
760        let refcount_offset =
761            (rb_offset + (second.rb_index(rb_bits) * refcount_bytes) as u64) as usize;
762        let data = state.data.lock();
763        let refcount = u16::from_be_bytes(
764            data[refcount_offset..refcount_offset + refcount_bytes]
765                .try_into()
766                .unwrap(),
767        );
768        assert_eq!(
769            refcount, 1,
770            "refcount increment for {second:?} lost on disk"
771        );
772    }
773
774    /// Fully discarding (to the backing file) must work on qcow2 v2 images, too, because it only
775    /// requires setting L2 entries to 0.
776    #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
777    async fn test_full_discard_v2() {
778        // Create a v3 image, then turn it into a v2 image: A v2 header is the same, except for the
779        // version, and all v3 fields having their default values (which imago creates by default,
780        // e.g. 16-bit refcounts)
781        let state = Arc::new(MemStorageState::default());
782        let qcow2 = create_image(&state, 64 * 1024, 16).await;
783        qcow2.flush().await.unwrap();
784        drop(qcow2);
785        state.data.lock()[4..8].copy_from_slice(&2u32.to_be_bytes());
786
787        let qcow2 = Qcow2::<MemStorage>::builder(MemStorage::new(state, false))
788            .backing(None)
789            .write(true)
790            .open(DenyImplicitOpenGate::default())
791            .await
792            .unwrap();
793        qcow2.header.require_version(2).unwrap();
794        qcow2.header.require_version(3).unwrap_err();
795        let mut image = FormatAccess::new(qcow2);
796
797        image.write(&[0x11u8; 512][..], 0).await.unwrap();
798        let (
799            Mapping::Raw {
800                offset: discarded_offset,
801                ..
802            },
803            _,
804        ) = image.get_mapping(0, 512).await.unwrap()
805        else {
806            panic!("Expected a data mapping");
807        };
808
809        image.discard_to_backing(0, 512).await.unwrap();
810        let (mapping, _) = image.get_mapping(0, 512).await.unwrap();
811        assert!(
812            !matches!(mapping, Mapping::Raw { .. }),
813            "Discarded cluster still mapped: {mapping:?}"
814        );
815
816        // The discarded cluster has been freed, so the next allocation can reuse it
817        image.write(&[0x22u8; 512][..], 512).await.unwrap();
818        let (Mapping::Raw { offset, .. }, _) = image.get_mapping(512, 512).await.unwrap() else {
819            panic!("Expected a data mapping");
820        };
821        assert_eq!(offset, discarded_offset);
822    }
823
824    /// Establishing mappings beyond the disk size must fail, also when starting at offset 0.
825    #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
826    async fn test_mapping_beyond_disk_size() {
827        let size = 64 * 1024;
828        let qcow2 = create_image(&Arc::default(), size, 16).await;
829        let length = size + 512;
830
831        let check = |result: io::Result<_>, req| match result {
832            Ok(_) => panic!("{req} of {length} bytes at 0 beyond the disk size ({size}) succeeded"),
833            Err(err) => assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof, "{req}: {err}"),
834        };
835
836        check(
837            qcow2
838                .ensure_data_mapping(0, length, false)
839                .await
840                .map(|_| ()),
841            "ensure_data_mapping",
842        );
843        check(
844            qcow2.ensure_zero_mapping(0, length).await.map(|_| ()),
845            "ensure_zero_mapping",
846        );
847        // Safe: Nothing is accessing the image concurrently
848        check(
849            unsafe { qcow2.discard_to_zero_unsafe(0, length) }
850                .await
851                .map(|_| ()),
852            "discard_to_zero_unsafe",
853        );
854        // Safe: Nothing is accessing the image concurrently
855        check(
856            unsafe { qcow2.discard_to_backing_unsafe(0, length) }
857                .await
858                .map(|_| ()),
859            "discard_to_backing_unsafe",
860        );
861    }
862
863    /// A prepared data mapping must become visible only once it is committed; dropping it instead
864    /// must leave the range as it was, and free the cluster allocated for it.
865    #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
866    async fn test_prepare_commit_data_mapping() {
867        let state = Arc::new(MemStorageState::default());
868        let image = FormatAccess::new(create_image(&state, 64 * 1024, 16).await);
869        let mut buf = [0xffu8; 512];
870
871        // Allocate guest cluster 0 (so the L2 table exists)
872        image.write(&[0x11u8; 512][..], 0).await.unwrap();
873
874        // Prepare a mapping for guest cluster 1 and write data to it, but then drop it
875        let mapping = image.prepare_data_mapping(512, 512, true).await.unwrap();
876        assert!(!mapping.is_nonblocking());
877        assert_eq!(mapping.length(), 512);
878        let host_offset = mapping.offset();
879        mapping
880            .storage()
881            .write(&[0x22u8; 512][..], host_offset)
882            .await
883            .unwrap();
884
885        image.read(&mut buf[..], 512).await.unwrap();
886        assert!(buf == [0u8; 512], "Uncommitted mapping is visible");
887
888        drop(mapping);
889        image.read(&mut buf[..], 512).await.unwrap();
890        assert!(buf == [0u8; 512], "Dropped mapping is visible");
891
892        // Flushing frees the dropped mapping’s cluster, so preparing a mapping again reuses it;
893        // commit it this time
894        image.flush().await.unwrap();
895        let mapping = image.prepare_data_mapping(512, 512, true).await.unwrap();
896        assert!(!mapping.is_nonblocking());
897        assert_eq!(mapping.offset(), host_offset);
898        mapping
899            .storage()
900            .write(&[0x33u8; 512][..], host_offset)
901            .await
902            .unwrap();
903        image.commit_data_mapping(mapping).await.unwrap();
904
905        image.read(&mut buf[..], 512).await.unwrap();
906        assert!(buf == [0x33u8; 512], "Committed mapping is not visible");
907
908        // Now that the cluster is mapped, there is nothing to commit
909        let mapping = image.prepare_data_mapping(512, 512, true).await.unwrap();
910        assert!(mapping.is_nonblocking());
911        assert_eq!(mapping.offset(), host_offset);
912        image.commit_data_mapping(mapping).await.unwrap();
913    }
914
915    /// A prepared data mapping must be rejected by an image it does not belong to (and discarded).
916    #[maybe_async::test(feature = "sync", async(feature = "async", tokio::test))]
917    async fn test_commit_data_mapping_on_other_image() {
918        let state = Arc::new(MemStorageState::default());
919        let image = FormatAccess::new(create_image(&state, 64 * 1024, 16).await);
920        let other_state = Arc::new(MemStorageState::default());
921        let other_image = FormatAccess::new(create_image(&other_state, 64 * 1024, 16).await);
922
923        let mapping = image.prepare_data_mapping(0, 512, true).await.unwrap();
924        assert!(!mapping.is_nonblocking());
925        let err = other_image.commit_data_mapping(mapping).await.unwrap_err();
926        assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
927
928        // Nothing has been mapped in either image
929        for image in [&image, &other_image] {
930            let (mapping, _) = image.get_mapping(0, 512).await.unwrap();
931            assert!(
932                matches!(mapping, Mapping::Zero { explicit: false }),
933                "Cluster has been mapped"
934            );
935        }
936    }
937
938    /// Read a big-endian 64-bit value from the storage.
939    #[cfg(feature = "async")]
940    fn read_u64(state: &MemStorageState, offset: u64) -> u64 {
941        let data = state.data.lock();
942        let offset = offset as usize;
943        u64::from_be_bytes(data[offset..offset + 8].try_into().unwrap())
944    }
945
946    /// If a flush is dropped before it has written back a modified L2 table, the table must still
947    /// be considered modified, so that a later flush writes it.
948    #[cfg(feature = "async")]
949    #[tokio::test]
950    async fn test_dropped_l2_writeback_keeps_table_modified() {
951        use crate::StorageExt;
952        use futures::FutureExt;
953
954        let state = Arc::new(MemStorageState::default());
955        let qcow2 = create_image(&state, 64 * 1024, 16).await;
956        let cb = qcow2.header.cluster_bits();
957
958        // Map guest cluster 0 (so the L2 table exists), and write everything
959        let mapping = qcow2.prepare_data_mapping(0, 512, true).await.unwrap();
960        qcow2.commit_data_mapping(mapping).await.unwrap();
961        qcow2.flush().await.unwrap();
962
963        // Modify the L2 table: Map guest cluster 1
964        let mapping = qcow2.prepare_data_mapping(512, 512, true).await.unwrap();
965        let host_offset = mapping.offset();
966        qcow2.commit_data_mapping(mapping).await.unwrap();
967
968        let l2_offset = {
969            let l1 = qcow2.l1_table.read().await;
970            l1.get(GuestOffset(0).l1_index(cb)).l2_offset().unwrap().0
971        };
972
973        // Flush, but have writing the L2 table wait for a concurrent request blocking its range,
974        // and drop the flush while it waits
975        let blocker = qcow2
976            .metadata
977            .strong_write_blocker(l2_offset..(l2_offset + 512))
978            .await;
979        assert!(qcow2.flush().now_or_never().is_none());
980        drop(blocker);
981
982        // A later flush must write the L2 table
983        qcow2.flush().await.unwrap();
984        let entry = read_u64(&state, l2_offset + 8);
985        assert_eq!(
986            entry & 0x00ff_ffff_ffff_fe00,
987            host_offset,
988            "mapping guest cluster 1 lost on disk (L2 entry {entry:#x})"
989        );
990    }
991
992    /// If a flush is dropped before it has written back a modified refblock, the refblock must
993    /// still be considered modified, so that a later flush writes it.
994    #[cfg(feature = "async")]
995    #[tokio::test]
996    async fn test_dropped_refblock_writeback_keeps_block_modified() {
997        use crate::StorageExt;
998        use futures::FutureExt;
999
1000        let state = Arc::new(MemStorageState::default());
1001        let qcow2 = create_image(&state, 64 * 1024, 16).await;
1002        let cb = qcow2.header.cluster_bits();
1003        let rb_bits = qcow2.header.rb_bits();
1004        let refcount_bytes = 2; // 16-bit refcounts
1005
1006        // Map guest cluster 0 (so the L2 table exists), and write everything
1007        let mapping = qcow2.prepare_data_mapping(0, 512, true).await.unwrap();
1008        qcow2.commit_data_mapping(mapping).await.unwrap();
1009        qcow2.flush().await.unwrap();
1010
1011        // Modify the refblock: Allocate a data cluster for guest cluster 1
1012        let mapping = qcow2.prepare_data_mapping(512, 512, true).await.unwrap();
1013        let host_offset = mapping.offset();
1014        qcow2.commit_data_mapping(mapping).await.unwrap();
1015        let host_cluster = HostOffset(host_offset).cluster(cb);
1016        assert_eq!(host_cluster.rt_index(rb_bits), 0);
1017
1018        let rb_offset = read_u64(&state, qcow2.header.reftable_offset().0) & !511;
1019
1020        // Flush, but have writing the refblock wait for a concurrent request blocking its range,
1021        // and drop the flush while it waits
1022        let blocker = qcow2
1023            .metadata
1024            .strong_write_blocker(rb_offset..(rb_offset + 512))
1025            .await;
1026        assert!(qcow2.flush().now_or_never().is_none());
1027        drop(blocker);
1028
1029        // A later flush must write the refblock
1030        qcow2.flush().await.unwrap();
1031        let refcount_offset =
1032            (rb_offset + (host_cluster.rb_index(rb_bits) * refcount_bytes) as u64) as usize;
1033        let data = state.data.lock();
1034        let refcount = u16::from_be_bytes(
1035            data[refcount_offset..refcount_offset + refcount_bytes]
1036                .try_into()
1037                .unwrap(),
1038        );
1039        assert_eq!(
1040            refcount, 1,
1041            "refcount increment for {host_cluster:?} lost on disk"
1042        );
1043    }
1044}