Skip to main content

disklayer_sqlite/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! SQLite-backed disk layer implementation.
5//!
6//! At this time, **this layer is only designed for use in dev/test scenarios!**
7//!
8//! # DISCLAIMER: Stability
9//!
10//! There are no stability guarantees around the on-disk data format! The schema
11//! can and will change without warning!
12//!
13//! # DISCLAIMER: Performance
14//!
15//! This implementation has only been minimally optimized! Don't expect to get
16//! incredible perf from this disk backend!
17//!
18//! Notably:
19//!
20//! - Data is stored within a single `sectors` table as tuples of `(sector:
21//!   INTEGER, sector_data: BLOB(sector_size))`. All data is accessed in
22//!   `sector_size` chunks (i.e: without performing any kind of adjacent-sector
23//!   coalescing).
24//! - Reads and writes currently allocate many temporary `Vec<u8>` buffers per
25//!   operation, without any buffer reuse.
26//!
27//! These design choices were made with simplicity and expediency in mind, given
28//! that the primary use-case for this backend is for dev/test scenarios. If
29//! performance ever becomes a concern, there are various optimizations that
30//! should be possible to implement here, though quite frankly, investing in a
31//! cross-platform QCOW2 or VHDX disk backend is likely a far more worthwhile
32//! endeavor.
33//!
34//! # Context
35//!
36//! In late 2024, OpenVMM was missing a _cross-platform_ disk backend that
37//! supported the following key features:
38//!
39//! - Used a dynamically-sized file as the disks's backing store
40//! - Supported snapshots / differencing disks
41//!
42//! While OpenVMM will eventually need to support for one or more of the current
43//! "industry standard" virtual disk formats that supports these features (e.g:
44//! QCOW2, VHDX), we really wanted some sort of "stop-gap" solution to unblock
45//! various dev/test use-cases.
46//!
47//! And thus, `disklayer_sqlite` was born!
48//!
49//! The initial implementation took less than a day to get up and running, and
50//! worked "well enough" to support the dev/test scenarios we were interested
51//! in, such as:
52//!
53//! - Having a cross-platform _sparsely allocated_ virtual disk file.
54//! - Having a _persistent_ diff-disk on-top of an existing disk (as opposed to
55//!   `ramdiff`, which is in-memory and _ephemeral_)
56//! - Having a "cache" layer for JIT-accessed disks, such as `disk_blob`
57//!
58//! The idea of using SQLite as a backing store - while wacky - proved to be an
59//! excellent way to quickly bring up a dynamically-sized, sparsely-allocated
60//! disk format for testing in OpenVMM.
61
62#![forbid(unsafe_code)]
63
64mod auto_cache;
65pub mod resolver;
66
67use anyhow::Context;
68use blocking::unblock;
69use disk_backend::DiskError;
70use disk_backend::UnmapBehavior;
71use disk_layered::LayerAttach;
72use disk_layered::LayerIo;
73use disk_layered::SectorMarker;
74use disk_layered::WriteNoOverwrite;
75use futures::lock::Mutex;
76use futures::lock::OwnedMutexGuard;
77use guestmem::MemoryRead;
78use guestmem::MemoryWrite;
79use inspect::Inspect;
80use rusqlite::Connection;
81use scsi_buffers::RequestBuffers;
82use std::path::Path;
83use std::path::PathBuf;
84use std::sync::Arc;
85
86/// Formatting parameters provided to [`FormatOnAttachSqliteDiskLayer::new`].
87///
88/// Optional parameters which are not provided will be determined by reading the
89/// metadata of the layer being attached to.
90#[derive(Inspect, Copy, Clone)]
91pub struct IncompleteFormatParams {
92    /// Should the layer be considered logically read only (i.e: a cache layer)
93    pub logically_read_only: bool,
94    /// The size of the layer in bytes.
95    pub len: Option<u64>,
96}
97
98/// Formatting parameters provided to [`SqliteDiskLayer::new`]
99#[derive(Inspect, Copy, Clone)]
100pub struct FormatParams {
101    /// Should the layer be considered logically read only (i.e: a cache layer)
102    pub logically_read_only: bool,
103    /// The size of the layer in bytes. Must be divisible by `sector_size`.
104    pub len: u64,
105    /// The size of each sector.
106    pub sector_size: u32,
107}
108
109/// A disk layer backed by sqlite, which lazily infers its topology from the
110/// layer it is being stacked on-top of.
111pub struct FormatOnAttachSqliteDiskLayer {
112    dbhd_path: PathBuf,
113    read_only: bool,
114    format_dbhd: IncompleteFormatParams,
115}
116
117impl FormatOnAttachSqliteDiskLayer {
118    /// Create a new sqlite-backed disk layer, which is formatted when it is
119    /// attached.
120    pub fn new(dbhd_path: PathBuf, read_only: bool, format_dbhd: IncompleteFormatParams) -> Self {
121        Self {
122            dbhd_path,
123            read_only,
124            format_dbhd,
125        }
126    }
127}
128
129/// A disk layer backed entirely by sqlite.
130#[derive(Inspect)]
131pub struct SqliteDiskLayer {
132    #[inspect(skip)]
133    conn: Arc<Mutex<Connection>>, // FUTURE: switch to connection-pool instead
134    meta: schema::DiskMeta,
135}
136
137impl SqliteDiskLayer {
138    /// Create a new sqlite-backed disk layer.
139    pub fn new(
140        dbhd_path: &Path,
141        read_only: bool,
142        format_dbhd: Option<FormatParams>,
143    ) -> anyhow::Result<Self> {
144        // DEVNOTE: sqlite _really_ want to be in control of opening the file,
145        // since it also wants to read/write to the runtime "sidecar" files that
146        // get created when accessing the DB (i.e: the `*-shm` and `*-wal`
147        // files)
148        //
149        // This will make it tricky to sandbox SQLite in the future...
150        //
151        // One idea: maybe we could implement a small SQLite `vfs` shim that
152        // lets use pre-open those particular files on the caller side, and hand
153        // them to sqlite when requested (vs. having it `open()` them itself?)
154        let conn = Connection::open_with_flags(dbhd_path, {
155            use rusqlite::OpenFlags;
156
157            let mut flags = OpenFlags::SQLITE_OPEN_NO_MUTEX;
158
159            if read_only {
160                flags |= OpenFlags::SQLITE_OPEN_READ_ONLY;
161            } else {
162                flags |= OpenFlags::SQLITE_OPEN_READ_WRITE;
163            }
164
165            // FUTURE: if/when the VFS layer is implemented, it _may_ be worth
166            // removing this flag entirely, and relying on the VFS to ensure
167            // that the (possibly blank) db file has been created. Emphasis on
168            // the word "may", as its unclear what the best approach will be
169            // until if/when we have more of the VFS infrastructure in place.
170            if format_dbhd.is_some() {
171                flags |= OpenFlags::SQLITE_OPEN_CREATE
172            }
173
174            flags
175        })?;
176
177        let meta = if let Some(FormatParams {
178            logically_read_only,
179            len,
180            sector_size,
181        }) = format_dbhd
182        {
183            use rusqlite::config::DbConfig;
184
185            // Wipe any existing contents.
186            //
187            // see https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigresetdatabase
188            conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)?;
189            conn.execute("VACUUM", ())?;
190            conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)?;
191
192            // Set core database config, and initialize table structure
193            conn.pragma_update(None, "journal_mode", "WAL")?;
194            conn.execute(schema::DEFINE_TABLE_SECTORS, [])?;
195            conn.execute(schema::DEFINE_TABLE_METADATA, [])?;
196
197            if len % sector_size as u64 != 0 {
198                anyhow::bail!(
199                    "failed to format: len={len} must be multiple of sector_size={sector_size}"
200                );
201            }
202            let sector_count = len / sector_size as u64;
203
204            let meta = schema::DiskMeta {
205                logically_read_only,
206                sector_count,
207                sector_size,
208            };
209
210            conn.execute(
211                "INSERT INTO meta VALUES (json(?))",
212                [serde_json::to_string(&meta).unwrap()],
213            )?;
214
215            meta
216        } else {
217            use rusqlite::OptionalExtension;
218            let data: String = conn
219                .query_row("SELECT json_extract(metadata, '$') FROM meta", [], |row| {
220                    row.get(0)
221                })
222                .optional()?
223                .context("missing `meta` table")?;
224            serde_json::from_str(&data)?
225        };
226
227        Ok(SqliteDiskLayer {
228            conn: Arc::new(Mutex::new(conn)),
229            meta,
230        })
231    }
232
233    async fn write_maybe_overwrite(
234        &self,
235        buffers: &RequestBuffers<'_>,
236        sector: u64,
237        overwrite: bool,
238    ) -> Result<(), DiskError> {
239        assert!(!(overwrite && self.meta.logically_read_only));
240
241        let count = buffers.len() / self.meta.sector_size as usize;
242        tracing::trace!(sector, count, "write");
243
244        // Nothing downstream enforces this: SQLite will happily insert rows for
245        // sectors past the end of the disk.
246        if sector + count as u64 > self.meta.sector_count {
247            return Err(DiskError::IllegalBlock);
248        }
249
250        let buf = buffers.reader().read_all()?;
251        unblock({
252            let conn = self.conn.clone().lock_owned().await;
253            let sector_size = self.meta.sector_size;
254            move || write_sectors(conn, sector_size, sector, buf, overwrite)
255        })
256        .await
257        .map_err(|e| DiskError::Io(std::io::Error::other(e)))?;
258
259        Ok(())
260    }
261}
262
263impl LayerAttach for FormatOnAttachSqliteDiskLayer {
264    type Error = anyhow::Error;
265    type Layer = SqliteDiskLayer;
266
267    async fn attach(
268        self,
269        lower_layer_metadata: Option<disk_layered::DiskLayerMetadata>,
270    ) -> Result<Self::Layer, Self::Error> {
271        let len = {
272            let lower_len = lower_layer_metadata
273                .as_ref()
274                .map(|m| m.sector_count * m.sector_size as u64);
275            self.format_dbhd
276                .len
277                .or(lower_len)
278                .context("no base layer to infer sector_count from")?
279        };
280        // FUTURE: make sector-size configurable
281        let sector_size = lower_layer_metadata.map(|x| x.sector_size).unwrap_or(512);
282
283        SqliteDiskLayer::new(
284            &self.dbhd_path,
285            self.read_only,
286            Some(FormatParams {
287                logically_read_only: self.format_dbhd.logically_read_only,
288                len,
289                sector_size,
290            }),
291        )
292    }
293}
294
295impl LayerIo for SqliteDiskLayer {
296    fn layer_type(&self) -> &str {
297        "sqlite"
298    }
299
300    fn sector_count(&self) -> u64 {
301        self.meta.sector_count
302    }
303
304    fn sector_size(&self) -> u32 {
305        self.meta.sector_size
306    }
307
308    fn is_logically_read_only(&self) -> bool {
309        self.meta.logically_read_only
310    }
311
312    fn disk_id(&self) -> Option<[u8; 16]> {
313        None
314    }
315
316    fn physical_sector_size(&self) -> u32 {
317        self.meta.sector_size
318    }
319
320    fn is_fua_respected(&self) -> bool {
321        false
322    }
323
324    async fn read(
325        &self,
326        buffers: &RequestBuffers<'_>,
327        sector: u64,
328        mut marker: SectorMarker<'_>,
329    ) -> Result<(), DiskError> {
330        let sector_count = (buffers.len() / self.meta.sector_size as usize) as u64;
331        let end_sector = sector + sector_count;
332        tracing::trace!(sector, sector_count, "read");
333        if end_sector > self.meta.sector_count {
334            return Err(DiskError::IllegalBlock);
335        }
336
337        let valid_sectors = unblock({
338            let conn = self.conn.clone().lock_owned().await;
339            let end_sector = sector + sector_count;
340            let sector_size = self.meta.sector_size;
341            move || read_sectors(conn, sector_size, sector, end_sector)
342        })
343        .await
344        .map_err(|e| DiskError::Io(std::io::Error::other(e)))?;
345
346        for (s, data) in valid_sectors {
347            let offset = (s - sector) as usize * self.meta.sector_size as usize;
348            let subrange = buffers.subrange(offset, self.meta.sector_size as usize);
349            let mut writer = subrange.writer();
350            match data {
351                SectorKind::AllZero => writer.zero(self.meta.sector_size as usize)?,
352                SectorKind::Data(data) => writer.write(&data)?,
353            };
354
355            marker.set(s);
356        }
357
358        Ok(())
359    }
360
361    async fn write(
362        &self,
363        buffers: &RequestBuffers<'_>,
364        sector: u64,
365        _fua: bool,
366    ) -> Result<(), DiskError> {
367        self.write_maybe_overwrite(buffers, sector, true).await
368    }
369
370    fn write_no_overwrite(&self) -> Option<impl WriteNoOverwrite> {
371        Some(self)
372    }
373
374    async fn sync_cache(&self) -> Result<(), DiskError> {
375        tracing::trace!("sync_cache");
376
377        unblock({
378            let mut conn = self.conn.clone().lock_owned().await;
379            move || -> rusqlite::Result<()> {
380                // https://sqlite-users.sqlite.narkive.com/LX75NOma/forcing-a-manual-fsync-in-wal-normal-mode
381                conn.pragma_update(None, "synchronous", "FULL")?;
382                {
383                    let tx = conn.transaction()?;
384                    tx.pragma_update(None, "user_version", "0")?;
385                }
386                conn.pragma_update(None, "synchronous", "NORMAL")?;
387                Ok(())
388            }
389        })
390        .await
391        .map_err(|e| DiskError::Io(std::io::Error::other(e)))
392    }
393
394    async fn unmap(
395        &self,
396        sector_offset: u64,
397        sector_count: u64,
398        _block_level_only: bool,
399        next_is_zero: bool,
400    ) -> Result<(), DiskError> {
401        tracing::trace!(sector_offset, sector_count, "unmap");
402        if sector_offset + sector_count > self.meta.sector_count {
403            return Err(DiskError::IllegalBlock);
404        }
405
406        unblock({
407            let conn = self.conn.clone().lock_owned().await;
408            move || unmap_sectors(conn, sector_offset, sector_count, next_is_zero)
409        })
410        .await
411        .map_err(|e| DiskError::Io(std::io::Error::other(e)))?;
412
413        Ok(())
414    }
415
416    fn unmap_behavior(&self) -> UnmapBehavior {
417        UnmapBehavior::Zeroes
418    }
419
420    fn optimal_unmap_sectors(&self) -> u32 {
421        1
422    }
423}
424
425impl WriteNoOverwrite for SqliteDiskLayer {
426    async fn write_no_overwrite(
427        &self,
428        buffers: &RequestBuffers<'_>,
429        sector: u64,
430    ) -> Result<(), DiskError> {
431        self.write_maybe_overwrite(buffers, sector, false).await
432    }
433}
434
435enum SectorKind {
436    AllZero,
437    Data(Vec<u8>),
438}
439
440// FUTURE: read from sqlite directly into `RequestBuffers`.
441fn read_sectors(
442    conn: OwnedMutexGuard<Connection>,
443    sector_size: u32,
444    start_sector: u64,
445    end_sector: u64,
446) -> anyhow::Result<Vec<(u64, SectorKind)>> {
447    let mut select_stmt = conn.prepare_cached(
448        "SELECT sector, data
449        FROM sectors
450        WHERE sector >= ? AND sector < ?
451        ORDER BY sector ASC",
452    )?;
453    let mut rows = select_stmt.query(rusqlite::params![start_sector, end_sector])?;
454
455    let mut res = Vec::new();
456    while let Some(row) = rows.next()? {
457        let sector: u64 = row.get(0)?;
458        let data: Option<&[u8]> = row.get_ref(1)?.as_blob_or_null()?;
459        let data = if let Some(data) = data {
460            if data.len() != sector_size as usize {
461                anyhow::bail!(
462                    "db contained sector with unexpected size (expected={}, found={}, sector={:#x})",
463                    sector_size,
464                    data.len(),
465                    sector
466                )
467            }
468            SectorKind::Data(data.into())
469        } else {
470            SectorKind::AllZero
471        };
472        res.push((sector, data));
473    }
474
475    Ok(res)
476}
477
478// FUTURE: write into sqlite directly from `RequestBuffers`.
479fn write_sectors(
480    mut conn: OwnedMutexGuard<Connection>,
481    sector_size: u32,
482    mut sector: u64,
483    buf: Vec<u8>,
484    overwrite: bool,
485) -> Result<(), rusqlite::Error> {
486    let tx = conn.transaction()?;
487    {
488        let mut stmt = if overwrite {
489            tx.prepare_cached("INSERT OR REPLACE INTO sectors (sector, data) VALUES (?, ?)")?
490        } else {
491            tx.prepare_cached("INSERT OR IGNORE INTO sectors (sector, data) VALUES (?, ?)")?
492        };
493
494        let chunks = buf.chunks_exact(sector_size as usize);
495        assert!(chunks.remainder().is_empty());
496        for chunk in chunks {
497            if chunk.iter().all(|x| *x == 0) {
498                stmt.execute(rusqlite::params![sector, rusqlite::types::Null])?;
499            } else {
500                stmt.execute(rusqlite::params![sector, chunk])?;
501            };
502
503            sector += 1;
504        }
505    }
506    tx.commit()?;
507
508    Ok(())
509}
510
511fn unmap_sectors(
512    mut conn: OwnedMutexGuard<Connection>,
513    sector_offset: u64,
514    sector_count: u64,
515    next_is_zero: bool,
516) -> Result<(), rusqlite::Error> {
517    if next_is_zero {
518        let mut clear_stmt =
519            conn.prepare_cached("DELETE FROM sectors WHERE sector BETWEEN ? AND ?")?;
520        clear_stmt.execute(rusqlite::params![
521            sector_offset,
522            sector_offset + sector_count - 1
523        ])?;
524    } else {
525        let tx = conn.transaction()?;
526        {
527            let mut stmt =
528                tx.prepare_cached("INSERT OR REPLACE INTO sectors (sector, data) VALUES (?, ?)")?;
529
530            for sector in sector_offset..(sector_offset + sector_count) {
531                stmt.execute(rusqlite::params![sector, rusqlite::types::Null])?;
532            }
533        }
534        tx.commit()?;
535    }
536
537    Ok(())
538}
539
540mod schema {
541    use inspect::Inspect;
542    use serde::Deserialize;
543    use serde::Serialize;
544
545    // DENOTE: SQLite actually saves the _plaintext_ of CREATE TABLE
546    // statements in its file format, which makes it a pretty good place to
547    // stash inline comments about the schema being used
548    //
549    // DEVNOTE: the choice to use the len of the blob as a marker for all
550    // zero / all one sectors has not been profiled relative to other
551    // implementation (e.g: having a third "kind" column).
552    pub const DEFINE_TABLE_SECTORS: &str = r#"
553CREATE TABLE sectors (
554    -- if data is NULL, that indicates an all-zero sector.
555    -- otherwise, data has len == SECTOR_SIZE, containing the sector data.
556    sector INTEGER NOT NULL,
557    data   BLOB,
558    PRIMARY KEY (sector)
559)
560"#; // TODO?: enforce sqlite >3.37.0 so we can use STRICT
561
562    // DEVNOTE: Given that this is a singleton table, we might as well use JSON
563    // + serde to store whatever metadata we want here, vs. trying to bend our
564    // metadata structure to sqlite's native data types.
565    //
566    // Using JSON (vs, say, protobuf) has the added benefit of allowing existing
567    // external sqlite tooling to more easily read and manipulate the metadata
568    // using sqlite's built-in JSON handling functions.
569    pub const DEFINE_TABLE_METADATA: &str = r#"
570CREATE TABLE meta (
571    metadata TEXT NOT NULL -- stored as JSON
572)
573"#;
574
575    #[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize, Inspect)]
576    pub struct DiskMeta {
577        pub logically_read_only: bool,
578        pub sector_count: u64,
579        pub sector_size: u32,
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use crate::FormatParams;
586    use crate::SqliteDiskLayer;
587    use disk_backend::Disk;
588    use disk_layered::DiskLayer;
589    use disk_layered::LayerConfiguration;
590    use disk_layered::LayeredDisk;
591    use pal_async::async_test;
592
593    const SECTOR_SIZE: u32 = 512;
594    const DISK_SIZE: u64 = 1024 * 1024;
595
596    fn new_layer(dir: &tempfile::TempDir) -> SqliteDiskLayer {
597        SqliteDiskLayer::new(
598            &dir.path().join("test.dbhd"),
599            false,
600            Some(FormatParams {
601                logically_read_only: false,
602                len: DISK_SIZE,
603                sector_size: SECTOR_SIZE,
604            }),
605        )
606        .unwrap()
607    }
608
609    #[async_test]
610    async fn sector_range_conformance() {
611        let dir = tempfile::tempdir().unwrap();
612        let disk = Disk::new(
613            LayeredDisk::new(
614                false,
615                vec![LayerConfiguration {
616                    layer: DiskLayer::new(new_layer(&dir)),
617                    write_through: false,
618                    read_cache: false,
619                }],
620            )
621            .await
622            .unwrap(),
623        )
624        .unwrap();
625        storage_tests::sector_range::test_disk_sector_range_conformance(&disk).await;
626    }
627
628    /// `LayeredDisk` does not reach `write_no_overwrite` or both values of
629    /// `unmap`'s `next_is_zero`, so the layer is also tested directly.
630    #[async_test]
631    async fn layer_sector_range_conformance() {
632        let dir = tempfile::tempdir().unwrap();
633        storage_tests::sector_range::test_layer_sector_range(&new_layer(&dir)).await;
634    }
635}