mirror of
https://github.com/vhaudiquet/ecrou.git
synced 2026-11-12 22:22:44 +00:00
fix(image): publish the built image atomically
mke2fs creates and sizes the output file before it populates it, so a build that failed mid-way left a recent-looking image full of zeros at the final name - newer than the tarball, and trusted by the freshness check forever after. The first run after the libarchive fix booted exactly that wreckage: EXT4-fs "Can't find ext4 filesystem". The build now lands in a sibling .part file and renames over the image only on success, and the freshness check additionally requires the ext4 superblock magic (s_magic, at byte 1080) before trusting an existing image - caches already carrying wreckage rebuild over it on the next run instead of needing a manual clean.
This commit is contained in:
1 parent
0347309eea
commit
4affe9230c
2 files changed
+76
-5
No files matched your search
@@ -50,9 +50,49 @@ pub(crate) fn build_rootfs_image(
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run mke2fs over the archive. The filesystem size and inode count
|
/// Whether `image` carries an ext4 superblock: the superblock starts
|
||||||
/// come from a scan of the archive: mke2fs derives neither from a tar.
|
/// at byte 1024 of the file and s_magic sits 56 bytes into it, so the
|
||||||
|
/// pair is at byte 1080, little-endian. Anything else is the remains
|
||||||
|
/// of an interrupted or failed build, and the freshness check
|
||||||
|
/// rebuilds over it instead of booting zeros.
|
||||||
|
pub(crate) fn looks_like_ext4(image: &Path) -> bool {
|
||||||
|
use std::io::{Read, Seek};
|
||||||
|
let mut magic = [0u8; 2];
|
||||||
|
std::fs::File::open(image)
|
||||||
|
.and_then(|mut f| {
|
||||||
|
f.seek(std::io::SeekFrom::Start(1080))?;
|
||||||
|
f.read_exact(&mut magic)?;
|
||||||
|
Ok(magic)
|
||||||
|
})
|
||||||
|
.map(|m| m == [0x53, 0xEF])
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the entry image: mke2fs consumes the archive (merged first
|
||||||
|
/// when the tarball is an OCI layer bundle) and the result publishes
|
||||||
|
/// atomically.
|
||||||
fn mkfs_from_tar(tarball: &Path, image: &Path) -> anyhow::Result<()> {
|
fn mkfs_from_tar(tarball: &Path, image: &Path) -> anyhow::Result<()> {
|
||||||
|
// The build lands in a sibling file first and renames over the
|
||||||
|
// image only on success. mke2fs creates and sizes the file before
|
||||||
|
// it populates, so a failed build would otherwise leave a
|
||||||
|
// recent-looking image full of zeros at the final name — newer
|
||||||
|
// than the tarball it came from, and trusted by the freshness
|
||||||
|
// check forever after.
|
||||||
|
let staging = image.with_extension("part");
|
||||||
|
let built = build_ext4(tarball, &staging);
|
||||||
|
if built.is_ok() {
|
||||||
|
std::fs::rename(&staging, image)
|
||||||
|
.with_context(|| format!("publishing {}", image.display()))?;
|
||||||
|
} else {
|
||||||
|
let _ = std::fs::remove_file(&staging);
|
||||||
|
}
|
||||||
|
built
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create `image`, size it from a scan of the archive, and run mke2fs
|
||||||
|
/// over it. The filesystem size and inode count come from the scan:
|
||||||
|
/// mke2fs derives neither from a tar.
|
||||||
|
fn build_ext4(tarball: &Path, image: &Path) -> anyhow::Result<()> {
|
||||||
let (bytes, entries) = tar_scan(tarball)?;
|
let (bytes, entries) = tar_scan(tarball)?;
|
||||||
// Headroom for the journal, the inode tables and block rounding.
|
// Headroom for the journal, the inode tables and block rounding.
|
||||||
// The file is created sparse, so generous headroom costs nothing
|
// The file is created sparse, so generous headroom costs nothing
|
||||||
@@ -265,6 +305,28 @@ mod tests {
|
|||||||
build_rootfs_image(tarball, image, &crate::progress::NoProgress).unwrap();
|
build_rootfs_image(tarball, image, &crate::progress::NoProgress).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_file_without_the_magic_is_not_an_image() {
|
||||||
|
let work = tempfile::tempdir().unwrap();
|
||||||
|
let path = work.path().join("img");
|
||||||
|
// Freshly truncated: allocated but never populated.
|
||||||
|
std::fs::write(&path, vec![0u8; 4096]).unwrap();
|
||||||
|
assert!(!looks_like_ext4(&path));
|
||||||
|
// Too short to hold a superblock at all.
|
||||||
|
std::fs::write(&path, vec![0u8; 512]).unwrap();
|
||||||
|
assert!(!looks_like_ext4(&path));
|
||||||
|
// The magic, in place and little-endian.
|
||||||
|
let mut bytes = vec![0u8; 4096];
|
||||||
|
bytes[1024] = 0x00;
|
||||||
|
bytes[1025] = 0x08;
|
||||||
|
bytes[1080] = 0x53;
|
||||||
|
bytes[1081] = 0xEF;
|
||||||
|
std::fs::write(&path, bytes).unwrap();
|
||||||
|
assert!(looks_like_ext4(&path));
|
||||||
|
// Absent files are not images either.
|
||||||
|
assert!(!looks_like_ext4(&work.path().join("absent")));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_image_carries_the_tarball_ownership() {
|
fn the_image_carries_the_tarball_ownership() {
|
||||||
if !have_e2fsprogs() {
|
if !have_e2fsprogs() {
|
||||||
@@ -277,6 +339,11 @@ mod tests {
|
|||||||
let image = work.path().join("rootfs.img");
|
let image = work.path().join("rootfs.img");
|
||||||
build(&tarball, &image);
|
build(&tarball, &image);
|
||||||
|
|
||||||
|
// A finished build publishes an ext4 image and leaves no
|
||||||
|
// staging file behind.
|
||||||
|
assert!(looks_like_ext4(&image), "the built image lacks the magic");
|
||||||
|
assert!(!work.path().join("rootfs.part").exists());
|
||||||
|
|
||||||
let out = Command::new("debugfs")
|
let out = Command::new("debugfs")
|
||||||
.arg("-R")
|
.arg("-R")
|
||||||
.arg("stat /etc/marker")
|
.arg("stat /etc/marker")
|
||||||
|
|||||||
@@ -95,15 +95,19 @@ impl RootfsCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The entry's image, built from the entry tarball when absent or
|
/// The entry's image, built from the entry tarball when absent or
|
||||||
/// older than the tarball. Runs copy the image; the tarball is the
|
/// older than the tarball, or when the file on disk is not an ext4
|
||||||
/// build input only. One writer per entry, like the download path.
|
/// image at all — the remains of a build that died before its
|
||||||
|
/// atomic rename. Runs copy the image; the tarball is the build
|
||||||
|
/// input only. One writer per entry, like the download path.
|
||||||
fn ensure_image(&self, tarball: &Path, image: &Path, progress: &dyn Progress) -> Result<()> {
|
fn ensure_image(&self, tarball: &Path, image: &Path, progress: &dyn Progress) -> Result<()> {
|
||||||
let mtime = |p: &Path| {
|
let mtime = |p: &Path| {
|
||||||
std::fs::metadata(p)
|
std::fs::metadata(p)
|
||||||
.and_then(|m| m.modified())
|
.and_then(|m| m.modified())
|
||||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||||
};
|
};
|
||||||
let fresh = || image.exists() && mtime(image) >= mtime(tarball);
|
let fresh = || {
|
||||||
|
image.exists() && crate::image::looks_like_ext4(image) && mtime(image) >= mtime(tarball)
|
||||||
|
};
|
||||||
if fresh() {
|
if fresh() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user