1use std::os::unix::io::AsRawFd;
10use std::path::Path;
11use tracing::{debug, info, warn};
12
13pub const FS_VERITY_HASH_ALG_SHA256: u32 = 1;
15pub const FS_VERITY_HASH_ALG_SHA512: u32 = 2;
16
17const FS_IOC_ENABLE_VERITY: libc::c_ulong = 0x40806685;
19
20const FS_IOC_MEASURE_VERITY: libc::c_ulong = 0xC0046686;
22
23#[derive(Debug, thiserror::Error)]
25pub enum FsVerityError {
26 #[error("fs-verity not supported on this filesystem")]
27 NotSupported,
28 #[error("file has open writable descriptors")]
29 FileInUse,
30 #[error("I/O error: {0}")]
31 Io(#[from] std::io::Error),
32}
33
34#[derive(Debug, Default)]
36pub struct SealStats {
37 pub sealed: u32,
38 pub skipped: u32,
39 pub already_sealed: u32,
40 pub errors: u32,
41}
42
43#[repr(C)]
45struct FsVerityEnableArg {
46 version: u32,
47 hash_algorithm: u32,
48 block_size: u32,
49 salt_size: u32,
50 salt_ptr: u64,
51 sig_size: u32,
52 __reserved1: u32,
53 sig_ptr: u64,
54 __reserved2: [u64; 11],
55}
56
57#[repr(C)]
59struct FsVerityDigest {
60 digest_algorithm: u16,
61 digest_size: u16,
62 digest: [u8; 64], }
64
65pub fn seal_with_fsverity(path: &Path, algorithm: u32) -> Result<(), FsVerityError> {
73 let file = std::fs::OpenOptions::new()
74 .read(true)
75 .open(path)
76 .map_err(FsVerityError::Io)?;
77
78 let arg = FsVerityEnableArg {
79 version: 1,
80 hash_algorithm: algorithm,
81 block_size: 4096,
82 salt_size: 0,
83 salt_ptr: 0,
84 sig_size: 0,
85 __reserved1: 0,
86 sig_ptr: 0,
87 __reserved2: [0; 11],
88 };
89
90 let ret = unsafe {
93 libc::ioctl(file.as_raw_fd(), FS_IOC_ENABLE_VERITY, &arg)
94 };
95
96 if ret != 0 {
97 let err = std::io::Error::last_os_error();
98 match err.raw_os_error() {
99 Some(libc::EOPNOTSUPP) | Some(libc::ENOTTY) => {
100 return Err(FsVerityError::NotSupported);
101 }
102 Some(libc::EBUSY) | Some(libc::ETXTBSY) => {
103 return Err(FsVerityError::FileInUse);
104 }
105 Some(libc::EEXIST) => {
106 debug!("fs-verity already enabled: {:?}", path);
108 return Ok(());
109 }
110 _ => return Err(FsVerityError::Io(err)),
111 }
112 }
113
114 debug!("fs-verity sealed: {:?}", path);
115 Ok(())
116}
117
118pub fn is_verity_enabled(path: &Path) -> bool {
120 let file = match std::fs::OpenOptions::new().read(true).open(path) {
121 Ok(f) => f,
122 Err(_) => return false,
123 };
124
125 let mut digest = FsVerityDigest {
126 digest_algorithm: 0,
127 digest_size: 64,
128 digest: [0u8; 64],
129 };
130
131 let ret = unsafe {
134 libc::ioctl(file.as_raw_fd(), FS_IOC_MEASURE_VERITY, &mut digest)
135 };
136
137 ret == 0
138}
139
140pub fn seal_snapshot_directory(dir: &Path, algorithm: u32) -> Result<SealStats, FsVerityError> {
146 let mut stats = SealStats::default();
147 let mut verity_supported = true;
148
149 for entry in walkdir::WalkDir::new(dir)
150 .into_iter()
151 .filter_map(|e| e.ok())
152 .filter(|e| e.file_type().is_file())
153 {
154 if !verity_supported {
155 stats.skipped += 1;
156 continue;
157 }
158
159 match seal_with_fsverity(entry.path(), algorithm) {
160 Ok(()) => stats.sealed += 1,
161 Err(FsVerityError::NotSupported) => {
162 info!("fs-verity not supported on {:?}, skipping remaining files", dir);
163 verity_supported = false;
164 stats.skipped += 1;
165 }
166 Err(FsVerityError::FileInUse) => {
167 warn!("fs-verity: file in use, skipping: {:?}", entry.path());
168 stats.skipped += 1;
169 }
170 Err(e) => {
171 warn!("fs-verity seal error on {:?}: {}", entry.path(), e);
172 stats.errors += 1;
173 }
174 }
175 }
176
177 if stats.sealed > 0 {
178 info!(
179 "fs-verity sealed {} files in {:?} ({} skipped, {} errors)",
180 stats.sealed, dir, stats.skipped, stats.errors
181 );
182 }
183
184 Ok(stats)
185}
186
187pub fn parse_algorithm(name: &str) -> Option<u32> {
189 match name.to_lowercase().as_str() {
190 "sha256" | "sha-256" => Some(FS_VERITY_HASH_ALG_SHA256),
191 "sha512" | "sha-512" => Some(FS_VERITY_HASH_ALG_SHA512),
192 _ => None,
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
199 use super::*;
200
201 #[test]
202 fn test_parse_algorithm() {
203 assert_eq!(parse_algorithm("sha256"), Some(FS_VERITY_HASH_ALG_SHA256));
204 assert_eq!(parse_algorithm("SHA-256"), Some(FS_VERITY_HASH_ALG_SHA256));
205 assert_eq!(parse_algorithm("sha512"), Some(FS_VERITY_HASH_ALG_SHA512));
206 assert_eq!(parse_algorithm("invalid"), None);
207 }
208
209 #[test]
210 fn test_seal_on_tmpfs_returns_not_supported() {
211 let dir = tempfile::tempdir().unwrap();
212 let file = dir.path().join("test.dat");
213 std::fs::write(&file, b"test content").unwrap();
214
215 match seal_with_fsverity(&file, FS_VERITY_HASH_ALG_SHA256) {
216 Err(FsVerityError::NotSupported) => {} Ok(()) => {} Err(e) => panic!("Unexpected error: {:?}", e),
219 }
220 }
221
222 #[test]
223 fn test_is_verity_enabled_on_normal_file() {
224 let dir = tempfile::tempdir().unwrap();
225 let file = dir.path().join("test.dat");
226 std::fs::write(&file, b"test content").unwrap();
227
228 assert!(!is_verity_enabled(&file));
230 }
231
232 #[test]
233 fn test_seal_snapshot_directory_empty() {
234 let dir = tempfile::tempdir().unwrap();
235 let stats = seal_snapshot_directory(dir.path(), FS_VERITY_HASH_ALG_SHA256).unwrap();
236 assert_eq!(stats.sealed, 0);
237 assert_eq!(stats.errors, 0);
238 }
239}