composefs/fsverity/
mod.rs

1//! Linux fs-verity support for integrity verification.
2//!
3//! This module provides complete fs-verity functionality including userspace
4//! digest computation, kernel ioctl interfaces for enabling and measuring
5//! verity, and hash value types for SHA-256 and SHA-512.
6
7mod digest;
8mod hashvalue;
9mod ioctl;
10
11pub use digest::FsVerityHasher;
12
13use std::{
14    fs::File,
15    io::{Error, Seek},
16    os::{
17        fd::{AsFd, BorrowedFd, OwnedFd},
18        unix::fs::PermissionsExt,
19    },
20};
21
22use rustix::fs::{open, openat, Mode, OFlags};
23use thiserror::Error;
24
25pub use hashvalue::{FsVerityHashValue, Sha256HashValue, Sha512HashValue};
26
27use crate::util::proc_self_fd;
28
29/// Measuring fsverity failed.
30#[derive(Error, Debug)] // can't derive PartialEq because of std::io::Error
31pub enum MeasureVerityError {
32    /// I/O operation failed.
33    #[error("{0}")]
34    Io(#[from] Error),
35    /// fs-verity is not enabled on the file.
36    #[error("fs-verity is not enabled on file")]
37    VerityMissing,
38    /// The filesystem does not support fs-verity.
39    #[error("fs-verity is not supported by filesystem")]
40    FilesystemNotSupported,
41    /// The hash algorithm does not match the expected algorithm.
42    #[error("Expected algorithm {expected}, found {found}")]
43    InvalidDigestAlgorithm {
44        /// The expected algorithm identifier.
45        expected: u16,
46        /// The actual algorithm identifier found.
47        found: u16,
48    },
49    /// The digest size does not match the expected size.
50    #[error("Expected digest size {expected}")]
51    InvalidDigestSize {
52        /// The expected digest size in bytes.
53        expected: u16,
54    },
55}
56
57/// Enabling fsverity failed.
58#[derive(Error, Debug)]
59pub enum EnableVerityError {
60    /// I/O operation failed.
61    #[error("{0}")]
62    Io(#[from] Error),
63    /// The filesystem does not support fs-verity.
64    #[error("Filesystem does not support fs-verity")]
65    FilesystemNotSupported,
66    /// fs-verity is already enabled on the file.
67    #[error("fs-verity is already enabled on file")]
68    AlreadyEnabled,
69    /// The file has an open writable file descriptor.
70    #[error("File is opened for writing")]
71    FileOpenedForWrite,
72}
73
74/// A verity comparison failed.
75#[derive(Error, Debug)]
76pub enum CompareVerityError {
77    /// Failed to measure the fs-verity digest.
78    #[error("failed to read verity")]
79    Measure(#[from] MeasureVerityError),
80    /// The measured digest does not match the expected digest.
81    #[error("Expected digest {expected} but found {found}")]
82    DigestMismatch {
83        /// The expected digest as a hex string.
84        expected: String,
85        /// The actual digest found as a hex string.
86        found: String,
87    },
88}
89
90/// Compute the fs-verity digest for a given block of data, in userspace.
91///
92/// The fs-verity digest is a cryptographic hash over the fs-verity descriptor, which itself
93/// contains the root hash of a Merkle tree with an arity determined by the chosen block size and
94/// the output size of the chosen hash algorithm.
95///
96/// It's possible to choose the hash algorithm (via the generic parameter) but the blocksize is
97/// currently hardcoded to 4096.  Salt is not supported.
98///
99/// See <https://www.kernel.org/doc/html/latest/filesystems/fsverity.html#file-digest-computation>
100///
101/// # Arguments:
102///
103///  * `data`: the data to hash
104pub fn compute_verity<H: FsVerityHashValue>(data: &[u8]) -> H {
105    digest::FsVerityHasher::<H, 12>::hash(data)
106}
107
108/// Enable fs-verity on the given file.
109///
110/// This essentially boils down to the FS_IOC_ENABLE_VERITY ioctl.
111///
112/// The file must be stored on a filesystem which supports fs-verity.  The file descriptor must be
113/// opened O_RDONLY and there must be no other writable file descriptors or mappings for the file.
114///
115/// It's possible to choose the hash algorithm (via the generic parameter) but the blocksize is
116/// currently hardcoded to 4096.  Salt is not supported.
117pub fn enable_verity_raw<H: FsVerityHashValue>(fd: impl AsFd) -> Result<(), EnableVerityError> {
118    ioctl::fs_ioc_enable_verity::<H>(fd)
119}
120
121/// Enable fs-verity on the given file, retrying if file is opened for writing.
122///
123/// This uses `enable_verity_raw()` and is subject to the same restrictions and features.
124///
125/// A common pattern with fsverity files is:
126///
127/// * Open a read-write file descriptor
128/// * Write data to the read-write file descriptor
129/// * Re-open the file descriptor as a new read-only descriptor
130/// * Close the read-write file descriptor
131/// * Enable fsverity on the read-only file descriptor
132///
133/// However, in a multi-threaded program, it is possible that another
134/// thread calls `fork()` while the read-write descriptor is valid,
135/// thus making a copy of the read-write descriptor.  If the forked
136/// process does not close the file descriptor either explicitly or by
137/// calling `exec()` via O_CLOEXEC, then attempting to enable fsverity
138/// on the read-only file descriptor will fail with ETXTBSY.  It is
139/// generally assumed that the file descriptor will be closed rather
140/// quickly under these circumstances, so this function will try to
141/// enable verity three times, pausing for one millisecond between
142/// attempts.
143pub fn enable_verity_with_retry<H: FsVerityHashValue>(
144    fd: impl AsFd,
145) -> Result<(), EnableVerityError> {
146    let mut attempt = 1;
147    loop {
148        match enable_verity_raw::<H>(&fd) {
149            Err(EnableVerityError::FileOpenedForWrite) if attempt < 3 => {
150                std::thread::sleep(std::time::Duration::from_millis(1));
151                attempt += 1;
152            }
153            other => return other,
154        }
155    }
156}
157
158/// Enable fs-verity on the given file.  If the given file cannot be
159/// enabled because it is opened as writable, then a new copy of the
160/// file will be returned instead.  No attempt is made to sync the
161/// copied file contents to disk, it is up to the caller to do so if
162/// desired.
163///
164/// Take special note that in the case where a copied file descriptor
165/// is returned, the returned file is created as a tempfile and is
166/// unlinked.  Presumably the caller should take care to make this
167/// file permanent, using a combination of `linkat` and `renameat` to
168/// replace the original file.
169///
170/// This uses `enable_verity_raw()` and `enable_verity_with_retry()`
171/// and is subject to the same restrictions.
172///
173/// # Arguments:
174/// * `dirfd`: A directory file descriptor, used to determine the placement (via O_TMPFILE) of the new file (if necessary).
175/// * `fd`: The file decriptor to enable verity on
176/// # Return Value:
177/// * `Ok(None)` is returned if verity was enabled on the original file
178/// * `Ok(Some(OwnedFd))` is returned if a copy was made
179pub fn enable_verity_maybe_copy<H: FsVerityHashValue>(
180    dirfd: impl AsFd,
181    fd: BorrowedFd,
182) -> Result<Option<OwnedFd>, EnableVerityError> {
183    match enable_verity_with_retry::<H>(&fd) {
184        Ok(()) => Ok(None),
185        Err(EnableVerityError::FileOpenedForWrite) => {
186            let fd = enable_verity_on_copy::<H>(dirfd, fd)?;
187            Ok(Some(fd))
188        }
189        Err(other) => Err(other),
190    }
191}
192
193/// Enable fs-verity on a new copy of `fd`, consuming `fd` and
194/// returning the new copy.  The copy is created via O_TMPFILE
195/// relative to `dirfd`.
196fn enable_verity_on_copy<H: FsVerityHashValue>(
197    dirfd: impl AsFd,
198    fd: BorrowedFd,
199) -> Result<OwnedFd, EnableVerityError> {
200    let fd = fd.try_clone_to_owned().map_err(EnableVerityError::Io)?;
201    let mut fd = File::from(fd);
202    let mode = fd.metadata()?.permissions().mode();
203
204    loop {
205        fd.rewind().map_err(EnableVerityError::Io)?;
206
207        let mut new_rw_fd = File::from(
208            openat(
209                &dirfd,
210                ".",
211                OFlags::CLOEXEC | OFlags::RDWR | OFlags::TMPFILE,
212                mode.into(),
213            )
214            .map_err(|e| EnableVerityError::Io(e.into()))?,
215        );
216
217        std::io::copy(&mut fd, &mut new_rw_fd)?;
218        let new_ro_fd = open(
219            proc_self_fd(&new_rw_fd),
220            OFlags::RDONLY | OFlags::CLOEXEC,
221            Mode::empty(),
222        )
223        .map_err(|e| EnableVerityError::Io(e.into()))?;
224        drop(new_rw_fd);
225        if enable_verity_with_retry::<H>(&new_ro_fd).is_ok() {
226            return Ok(new_ro_fd);
227        }
228    }
229}
230
231/// Measures fs-verity on the given file.
232///
233/// This essentially boils down to the FS_IOC_MEASURE_VERITY ioctl.
234///
235/// If the file has fs-verity enabled then the hash of the fs-verity descriptor is reported as the
236/// successful return value.  In this case, the kernel guarantees that the file content cannot
237/// possibly change for as long as the file descriptor exists.
238///
239/// If the file doesn't have fs-verity enabled then an error will be returned.
240///
241/// This function is generic over the hash algorithm, which means that you need to choose the
242/// expected hash algorithm in advance.  If the file has fs-verity enabled, but with a different
243/// hash algorithm, then this is also considered an error.
244///
245/// For a version of this function which returns an Option<> depending on if fs-verity is enabled
246/// or not, see `measure_verity_opt()`.
247///
248/// Simply measuring the fs-verity value of a file is not a common operation: you usually want to
249/// compare it to a value that you already know.  In that case, it's better to use the
250/// `compare_verity()` function in this module.
251pub fn measure_verity<H: FsVerityHashValue>(fd: impl AsFd) -> Result<H, MeasureVerityError> {
252    ioctl::fs_ioc_measure_verity(fd)
253}
254
255/// Measures fs-verity on the given file.
256///
257/// This essentially boils down to the FS_IOC_MEASURE_VERITY ioctl.
258///
259/// This is the `_opt()` variant of `measure_verity()`.  If the file doesn't have fs-verity
260/// enabled, or resides on a filesystem where fs-verity is unsupported, this function returns None.
261/// Other errors are still passed through.
262pub fn measure_verity_opt<H: FsVerityHashValue>(
263    fd: impl AsFd,
264) -> Result<Option<H>, MeasureVerityError> {
265    match ioctl::fs_ioc_measure_verity(fd) {
266        Ok(result) => Ok(Some(result)),
267        Err(MeasureVerityError::VerityMissing | MeasureVerityError::FilesystemNotSupported) => {
268            Ok(None)
269        }
270        Err(other) => Err(other),
271    }
272}
273
274/// Compare the fs-verity digest of the file versus the expected digest.
275///
276/// This calls `measure_verity()` and verifies that the result is equal to the expected value.
277///
278/// If this function returns successfully then the values match.  In this case, the kernel
279/// guarantees that the file content cannot possibly change for as long as the file descriptor
280/// exists.
281///
282/// If the file doesn't have fs-verity enabled, the hash value doesn't match, or if a different
283/// hash algorithm is in use, the comparison will fail.
284pub fn ensure_verity_equal(
285    fd: impl AsFd,
286    expected: &impl FsVerityHashValue,
287) -> Result<(), CompareVerityError> {
288    let found = measure_verity(fd)?;
289    if expected == &found {
290        Ok(())
291    } else {
292        Err(CompareVerityError::DigestMismatch {
293            expected: expected.to_hex(),
294            found: found.to_hex(),
295        })
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use std::{collections::BTreeSet, io::Write, os::unix::process::CommandExt, time::Duration};
302
303    use once_cell::sync::Lazy;
304    use rand::Rng;
305    use rustix::{
306        fd::OwnedFd,
307        fs::{open, Mode, OFlags},
308    };
309    use tempfile::{tempfile_in, TempDir};
310    use tokio::{task::JoinSet, time::Instant};
311
312    use crate::{test::tempdir, util::proc_self_fd};
313
314    use super::*;
315
316    static TEMPDIR: Lazy<TempDir> = Lazy::new(tempdir);
317    static TD_FD: Lazy<File> = Lazy::new(|| File::open(TEMPDIR.path()).unwrap());
318
319    fn tempfile() -> File {
320        tempfile_in(TEMPDIR.path()).unwrap()
321    }
322
323    fn rdonly_file_with(data: &[u8]) -> OwnedFd {
324        let mut file = tempfile();
325        file.write_all(data).unwrap();
326        file.sync_data().unwrap();
327        let fd = open(
328            proc_self_fd(&file),
329            OFlags::RDONLY | OFlags::CLOEXEC,
330            Mode::empty(),
331        )
332        .unwrap();
333        drop(file); // can't enable verity with outstanding writable fds
334        fd
335    }
336
337    fn empty_file_in_tmpdir(flags: OFlags, mode: Mode) -> (tempfile::TempDir, OwnedFd) {
338        let tmpdir = tempdir();
339        let path = tmpdir.path().join("empty");
340        let fd = open(path, OFlags::CLOEXEC | OFlags::CREATE | flags, mode).unwrap();
341        (tmpdir, fd)
342    }
343
344    #[test]
345    fn test_verity_missing() {
346        let tf = rdonly_file_with(b"");
347
348        assert!(matches!(
349            measure_verity::<Sha256HashValue>(&tf).unwrap_err(),
350            MeasureVerityError::VerityMissing
351        ));
352
353        assert!(measure_verity_opt::<Sha256HashValue>(&tf)
354            .unwrap()
355            .is_none());
356
357        assert!(matches!(
358            ensure_verity_equal(&tf, &Sha256HashValue::EMPTY).unwrap_err(),
359            CompareVerityError::Measure(MeasureVerityError::VerityMissing)
360        ));
361    }
362
363    #[test]
364    fn test_verity_simple() {
365        let tf = rdonly_file_with(b"hello world");
366
367        // first time: success
368        let tf = enable_verity_maybe_copy::<Sha256HashValue>(&*TD_FD, tf.as_fd())
369            .unwrap()
370            .unwrap_or(tf);
371
372        // second time: fail with "already enabled"
373        assert!(matches!(
374            enable_verity_maybe_copy::<Sha256HashValue>(&*TD_FD, tf.as_fd()).unwrap_err(),
375            EnableVerityError::AlreadyEnabled
376        ));
377
378        assert_eq!(
379            measure_verity::<Sha256HashValue>(&tf).unwrap().to_hex(),
380            "1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7d5f56362ba64"
381        );
382
383        assert_eq!(
384            measure_verity_opt::<Sha256HashValue>(&tf)
385                .unwrap()
386                .unwrap()
387                .to_hex(),
388            "1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7d5f56362ba64"
389        );
390
391        ensure_verity_equal(
392            &tf,
393            &Sha256HashValue::from_hex(
394                "1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7d5f56362ba64",
395            )
396            .unwrap(),
397        )
398        .unwrap();
399
400        let Err(CompareVerityError::DigestMismatch { expected, found }) = ensure_verity_equal(
401            &tf,
402            &Sha256HashValue::from_hex(
403                "1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7000000000000",
404            )
405            .unwrap(),
406        ) else {
407            panic!("Didn't fail with expected error");
408        };
409        assert_eq!(
410            expected,
411            "1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7000000000000"
412        );
413        assert_eq!(
414            found,
415            "1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7d5f56362ba64"
416        );
417    }
418
419    #[allow(unsafe_code)]
420    #[tokio::test]
421    async fn test_verity_forking() {
422        const DELAY_MIN: u64 = 0;
423        const DELAY_MAX: u64 = 10;
424        // Break if we get 100 successes
425        const SUCCESS_LIMIT: u32 = 100;
426        // Don't run more than 10s by default
427        const TIMEOUT: Duration = Duration::from_secs(10);
428        let start = Instant::now();
429
430        let cpus = std::thread::available_parallelism().unwrap();
431        // use half of capacity for forking
432        let threads = cpus.get() >> 1;
433        assert!(threads >= 1);
434        eprintln!("using {threads} threads");
435        let mut txs = vec![];
436        let mut jhs = vec![];
437
438        for _ in 0..threads {
439            let (tx, rx) = std::sync::mpsc::channel();
440            let jh = std::thread::spawn(move || {
441                let mut rng = rand::rng();
442
443                loop {
444                    if rx.try_recv().is_ok() {
445                        break;
446                    }
447
448                    let delay = rng.random_range(DELAY_MIN..=DELAY_MAX);
449                    let delay = Duration::from_millis(delay);
450                    unsafe {
451                        std::process::Command::new("true")
452                            .pre_exec(move || {
453                                std::thread::sleep(delay);
454                                Ok(())
455                            })
456                            .status()
457                            .unwrap();
458                    }
459                }
460            });
461
462            txs.push(tx);
463            jhs.push(jh);
464        }
465
466        let raw_verity_enabler = async move {
467            let mut successes = 0;
468            let mut failures = 0;
469
470            loop {
471                if tokio::time::Instant::now().duration_since(start) > TIMEOUT {
472                    break;
473                }
474                if successes == SUCCESS_LIMIT {
475                    break;
476                }
477
478                let r = tokio::task::spawn_blocking(move || {
479                    let ro_fd = rdonly_file_with(b"hello world");
480                    enable_verity_raw::<Sha256HashValue>(&ro_fd)
481                })
482                .await
483                .unwrap();
484                if r.is_ok() {
485                    successes += 1;
486                } else {
487                    failures += 1;
488                }
489            }
490
491            (successes, failures)
492        };
493
494        let retry_verity_enabler = async move {
495            let mut successes = 0;
496            let mut failures = 0;
497
498            loop {
499                if tokio::time::Instant::now().duration_since(start) > TIMEOUT {
500                    break;
501                }
502                if successes == SUCCESS_LIMIT {
503                    break;
504                }
505
506                let r = tokio::task::spawn_blocking(move || {
507                    let ro_fd = rdonly_file_with(b"hello world");
508                    enable_verity_with_retry::<Sha256HashValue>(&ro_fd)
509                })
510                .await
511                .unwrap();
512                if r.is_ok() {
513                    successes += 1;
514                } else {
515                    failures += 1;
516                }
517            }
518
519            (successes, failures)
520        };
521
522        let copy_verity_enabler = async move {
523            let mut orig = 0;
524            let mut copy = 0;
525
526            loop {
527                if tokio::time::Instant::now().duration_since(start) > TIMEOUT {
528                    break;
529                }
530                if orig + copy == SUCCESS_LIMIT {
531                    break;
532                }
533
534                let is_copy = tokio::task::spawn_blocking(|| {
535                    let ro_fd = rdonly_file_with(b"Hello world");
536                    enable_verity_maybe_copy::<Sha256HashValue>(&*TD_FD, ro_fd.as_fd())
537                        .unwrap()
538                        .is_some()
539                })
540                .await
541                .unwrap();
542
543                if is_copy {
544                    copy += 1;
545                } else {
546                    orig += 1;
547                }
548            }
549
550            (orig, copy)
551        };
552
553        let ts = tokio::time::Instant::now();
554
555        let mut set = JoinSet::new();
556        set.spawn(async move {
557            let (successes, failures) = raw_verity_enabler.await;
558            let elapsed = ts.elapsed().as_millis();
559            eprintln!("raw verity enabled ({successes} attempts succeeded, {failures} attempts failed) in {elapsed}ms");
560        });
561        set.spawn(async move {
562            let (successes, failures) = retry_verity_enabler.await;
563            let elapsed = ts.elapsed().as_millis();
564            eprintln!("retry verity enabled ({successes} attempts succeeded, {failures} attempts failed) in {elapsed}ms");
565        });
566        set.spawn(async move {
567            let (orig, copy) = copy_verity_enabler.await;
568            assert!(orig > 0 || copy > 0);
569            let elapsed = ts.elapsed().as_millis();
570            eprintln!("copy verity enabled ({orig} original, {copy} copies) in {elapsed}ms");
571        });
572
573        while let Some(res) = set.join_next().await {
574            res.unwrap();
575        }
576
577        txs.into_iter().for_each(|tx| tx.send(()).unwrap());
578        jhs.into_iter().for_each(|jh| jh.join().unwrap());
579    }
580
581    #[test_with::path(/dev/shm)]
582    #[test]
583    fn test_verity_error_noverity() {
584        let tf = tempfile_in("/dev/shm").unwrap();
585
586        assert!(matches!(
587            enable_verity_with_retry::<Sha256HashValue>(&tf).unwrap_err(),
588            EnableVerityError::FilesystemNotSupported
589        ));
590
591        assert!(matches!(
592            measure_verity::<Sha256HashValue>(&tf).unwrap_err(),
593            MeasureVerityError::FilesystemNotSupported
594        ));
595
596        assert!(measure_verity_opt::<Sha256HashValue>(&tf)
597            .unwrap()
598            .is_none());
599
600        assert!(matches!(
601            ensure_verity_equal(&tf, &Sha256HashValue::EMPTY).unwrap_err(),
602            CompareVerityError::Measure(MeasureVerityError::FilesystemNotSupported)
603        ));
604    }
605
606    #[test]
607    fn test_verity_wrongdigest_sha512_sha256() {
608        let tf = rdonly_file_with(b"hello world");
609
610        // Enable with SHA-512 but then try to read with SHA-256
611        let tf = enable_verity_maybe_copy::<Sha512HashValue>(&*TD_FD, tf.as_fd())
612            .unwrap()
613            .unwrap_or(tf);
614
615        assert!(matches!(
616            measure_verity::<Sha256HashValue>(&tf).unwrap_err(),
617            MeasureVerityError::InvalidDigestSize { .. }
618        ));
619
620        assert!(matches!(
621            measure_verity_opt::<Sha256HashValue>(&tf).unwrap_err(),
622            MeasureVerityError::InvalidDigestSize { .. }
623        ));
624
625        assert!(matches!(
626            ensure_verity_equal(&tf, &Sha256HashValue::EMPTY).unwrap_err(),
627            CompareVerityError::Measure(MeasureVerityError::InvalidDigestSize { .. })
628        ));
629    }
630
631    #[test]
632    fn test_verity_wrongdigest_sha256_sha512() {
633        let tf = rdonly_file_with(b"hello world");
634
635        // Enable with SHA-256 but then try to read with SHA-512
636        let tf = enable_verity_maybe_copy::<Sha256HashValue>(&*TD_FD, tf.as_fd())
637            .unwrap()
638            .unwrap_or(tf);
639
640        assert!(matches!(
641            measure_verity::<Sha512HashValue>(&tf).unwrap_err(),
642            MeasureVerityError::InvalidDigestAlgorithm { .. }
643        ));
644
645        assert!(matches!(
646            measure_verity_opt::<Sha512HashValue>(&tf).unwrap_err(),
647            MeasureVerityError::InvalidDigestAlgorithm { .. }
648        ));
649
650        assert!(matches!(
651            ensure_verity_equal(&tf, &Sha512HashValue::EMPTY).unwrap_err(),
652            CompareVerityError::Measure(MeasureVerityError::InvalidDigestAlgorithm { .. })
653        ));
654    }
655
656    #[test]
657    fn crosscheck_interesting_cases() {
658        // Test the kernel against our userspace calculations.
659        //
660        // We try to pick some "interesting" sizes to test the edge cases.  The arity of a
661        // SHA-256/4096 Merkle tree is 128 = 4096 / 32.  With SHA-512 it's 64 = 4096 / 64.
662        // So we try to chose values around the page size times powers of 32 and 64.
663        let mut cases = BTreeSet::new();
664        for arity in [32, 64] {
665            for layer4 in [/* -1, */ 0 /*, 1 */] {
666                /* otherwise it's too slow */
667                for layer3 in [-1, 0, 1] {
668                    for layer2 in [-1, 0, 1] {
669                        for layer1 in [-1, 0, 1] {
670                            for layer0 in [-1, 0, 1] {
671                                let candidate = layer4 * (arity * arity * arity * arity)
672                                    + layer3 * (arity * arity * arity)
673                                    + layer2 * (arity * arity)
674                                    + layer1 * arity
675                                    + layer0;
676                                if let Ok(size) = usize::try_from(candidate) {
677                                    cases.insert(size);
678                                }
679                            }
680                        }
681                    }
682                }
683            }
684        }
685
686        fn assert_kernel_equal<H: FsVerityHashValue>(data: &[u8], expected: H) {
687            let fd = rdonly_file_with(data);
688            let fd = enable_verity_maybe_copy::<H>(&*TD_FD, fd.as_fd())
689                .unwrap()
690                .unwrap_or(fd);
691            ensure_verity_equal(&fd, &expected).unwrap();
692        }
693
694        for size in cases {
695            // the actual data is uninteresting
696            let data = vec![0x5a; size];
697            assert_kernel_equal(&data, compute_verity::<Sha256HashValue>(&data));
698            assert_kernel_equal(&data, compute_verity::<Sha512HashValue>(&data));
699        }
700    }
701
702    #[test]
703    fn test_enable_verity_maybe_copy_without_copy() {
704        // Enabling verity on an empty file created without a
705        // read-write file descriptor ever existing should always
706        // succeed and hand us back the original file descriptor.
707        let (tempdir, fd) = empty_file_in_tmpdir(OFlags::RDONLY, 0o644.into());
708        let tempdir_fd = File::open(tempdir.path()).unwrap();
709        let fd = enable_verity_maybe_copy::<Sha256HashValue>(&tempdir_fd, fd.as_fd()).unwrap();
710        assert!(fd.is_none());
711    }
712
713    #[test]
714    fn test_enable_verity_maybe_copy_with_copy() {
715        // Here we intentionally try to enable verity on a read-write
716        // file descriptor, which will never work directly, so we
717        // expect to always get back a new copy of the requested file.
718        let (tempdir, fd) = empty_file_in_tmpdir(OFlags::RDWR, 0o644.into());
719        let tempdir_fd = File::open(tempdir.path()).unwrap();
720        let mut fd = File::from(fd);
721        let _ = fd.write(b"hello world").unwrap();
722        let fd = enable_verity_maybe_copy::<Sha256HashValue>(&tempdir_fd, fd.as_fd())
723            .unwrap()
724            .unwrap();
725
726        // The new fd has the correct data
727        assert!(ensure_verity_equal(
728            fd,
729            &Sha256HashValue::from_hex(
730                "1e2eaa4202d750a41174ee454970b92c1bc2f925b1e35076d8c7d5f56362ba64",
731            )
732            .unwrap(),
733        )
734        .is_ok());
735    }
736}