Skip to content

Commit 3d6c8c3

Browse files
etc-merge: Incremental hash computation + test verity
Test for whether the file has fs-verity enabled or not, and if it does we simply check the verity. Incrementally compute hash for files rather than reading the entire file in memory. Signed-off-by: Johan-Liebert1 <[email protected]>
1 parent 10d2c93 commit 3d6c8c3

File tree

3 files changed

+77
-14
lines changed

3 files changed

+77
-14
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/etc-merge/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ cap-std-ext = { workspace = true }
99
rustix = { workspace = true }
1010
openssl = { workspace = true }
1111
hex = { workspace = true }
12+
tracing = { workspace = true }
13+
composefs = { workspace = true }
1214

1315
[lints]
1416
workspace = true

crates/etc-merge/src/lib.rs

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,29 @@
22
33
#![allow(dead_code)]
44

5+
use std::io::BufReader;
56
use std::{collections::BTreeMap, io::Read, path::PathBuf};
67

78
use anyhow::Context;
89
use cap_std_ext::cap_std;
9-
use cap_std_ext::cap_std::fs::{Dir as CapStdDir, MetadataExt, PermissionsExt};
10+
use cap_std_ext::cap_std::fs::{Dir as CapStdDir, PermissionsExt};
11+
use composefs::fsverity::{FsVerityHashValue, Sha256HashValue};
1012
use openssl::hash::DigestBytes;
1113
use rustix::fs::readlinkat;
1214

1315
#[derive(Debug)]
1416
struct Metadata {
1517
content_hash: String,
1618
metadata_hash: String,
19+
verity: Option<String>,
1720
}
1821

1922
impl Metadata {
20-
fn new(content_hash: String, metadata_hash: String) -> Self {
23+
fn new(content_hash: String, metadata_hash: String, verity: Option<String>) -> Self {
2124
Self {
2225
content_hash,
2326
metadata_hash,
27+
verity,
2428
}
2529
}
2630
}
@@ -76,6 +80,26 @@ fn compute_diff(
7680
continue;
7781
};
7882

83+
match (&current_meta.verity, &old_meta.verity) {
84+
(Some(v1), Some(v2)) => {
85+
if v1 != v2 {
86+
modified.push(current_file.clone());
87+
pristine_etc_files.remove(&current_file);
88+
continue;
89+
}
90+
}
91+
92+
(None, None) => {
93+
// No verity enabled for files, so we move forward to checking metadata + content
94+
// checksum
95+
}
96+
97+
// This has to be some kind of error?
98+
(None, Some(_)) | (Some(_), None) => {
99+
anyhow::bail!("File did not have fs-verity now it does or vice-versa")
100+
}
101+
}
102+
79103
if old_meta.metadata_hash != current_meta.metadata_hash
80104
|| old_meta.content_hash != current_meta.content_hash
81105
{
@@ -134,7 +158,12 @@ fn recurse_dir(dir: &CapStdDir, mut path: PathBuf, list: &mut Map) -> anyhow::Re
134158

135159
list.insert(
136160
path.clone(),
137-
Metadata::new("".into(), hex::encode(compute_metadata_hash(&metadata)?)),
161+
Metadata::new(
162+
"".into(),
163+
hex::encode(compute_metadata_hash(&metadata)?),
164+
// fs-verity is not enabled for directories
165+
None,
166+
),
138167
);
139168

140169
recurse_dir(&dir, path.clone(), list).context(format!("Recursing {path:?}"))?;
@@ -143,27 +172,53 @@ fn recurse_dir(dir: &CapStdDir, mut path: PathBuf, list: &mut Map) -> anyhow::Re
143172
continue;
144173
}
145174

146-
let buf = if entry_type.is_symlink() {
175+
// TODO: Another generic here but constrained to Sha256HashValue
176+
// Regarding this, we'll definitely get DigestMismatch error if SHA512 is being used
177+
let measured_verity =
178+
composefs::fsverity::measure_verity_opt::<Sha256HashValue>(entry.open()?)?;
179+
180+
if let Some(measured_verity) = measured_verity {
181+
list.insert(
182+
path.clone(),
183+
Metadata::new("".into(), "".into(), Some(measured_verity.to_hex())),
184+
);
185+
186+
path.pop();
187+
188+
// file has fs-verity enabled. We don't need to check the content/metadata
189+
continue;
190+
}
191+
192+
let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())?;
193+
194+
if entry_type.is_symlink() {
147195
let readlinkat_result = readlinkat(&dir, &entry_name, vec![])
148196
.context(format!("readlinkat {entry_name:?}"))?;
149197

150-
readlinkat_result.into()
198+
hasher.update(readlinkat_result.as_bytes())?;
151199
} else if entry_type.is_file() {
152-
let mut buf = vec![0u8; entry.metadata()?.size() as usize];
200+
let mut buf = vec![0u8; 8192];
201+
202+
let file = entry.open().context(format!("Opening entry {path:?}"))?;
203+
let mut reader = BufReader::new(file);
153204

154-
let mut file = entry.open().context(format!("Opening entry {path:?}"))?;
155-
file.read_exact(&mut buf)
156-
.context(format!("Reading {path:?}. Buf: {buf:?}"))?;
205+
loop {
206+
let bytes_read = reader
207+
.read(&mut buf)
208+
.context(format!("Reading chunk from {path:?}"))?;
157209

158-
buf
210+
if bytes_read == 0 {
211+
break;
212+
}
213+
214+
hasher.update(&buf[..bytes_read])?;
215+
}
159216
} else {
160217
// We cannot read any other device like socket, pipe, fifo.
161218
// We shouldn't really find these in /etc in the first place
162-
unimplemented!("Found file of type {:?}", entry_type)
219+
tracing::info!("Ignoring non-regular/non-symlink file: {:?}", path);
163220
};
164221

165-
let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())?;
166-
hasher.update(&buf)?;
167222
let content_digest = hex::encode(hasher.finish()?);
168223

169224
let meta = entry
@@ -172,7 +227,11 @@ fn recurse_dir(dir: &CapStdDir, mut path: PathBuf, list: &mut Map) -> anyhow::Re
172227

173228
list.insert(
174229
path.clone(),
175-
Metadata::new(content_digest, hex::encode(compute_metadata_hash(&meta)?)),
230+
Metadata::new(
231+
content_digest,
232+
hex::encode(compute_metadata_hash(&meta)?),
233+
None,
234+
),
176235
);
177236

178237
path.pop();

0 commit comments

Comments
 (0)