Skip to content

fix(file)!: keep image holes, map the input bmap ranges, rename bmap options - #158

Open
JanZachmann wants to merge 55 commits into
omnect:mainfrom
JanZachmann:jz-2026-09-28-keep-image-layout
Open

JanZachmann wants to merge 55 commits into
omnect:mainfrom
JanZachmann:jz-2026-09-28-keep-image-layout

Conversation

@JanZachmann

@JanZachmann JanZachmann commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • write_partition compares the partition file with the image in 4 KiB blocks, only where one of them holds data, and writes only the blocks that differ, instead of running fallocate -d. It also writes a block that is data in the partition file but a hole in the image, e.g. a zero block of a file that e2cp or mcopy wrote into free space. fallocate, dd and sync are removed from the Docker image.
  • The image is copied with copy_image, and a partition is read out of the image with the same sparse copy instead of dd conv=sparse: it finds the data with SEEK_DATA/SEEK_HOLE, keeps the holes and copies each data range in a loop. Decompressing an image keeps zero blocks as holes, reads a file made of several streams (e.g. from pbzip2), and fails on a truncated or corrupt stream. A compressed image is decompressed straight into the temp dir, without a copy first. Images named .gz and .bz2 are decompressed too; before, the output file had the name of the source, so creating it emptied the source. Compressing fails when the end of the stream cannot be written.
  • omnect-cli writes the bmap of -b itself: it maps the ranges of the input bmap, or the whole image without one, plus every block the image file stores as data, with a sha256 per range. No zeros are written into the image, so it stays sparse, and bmaptool is no longer needed at runtime: -b now works in the Docker image, and bmap-tools is removed from the Debian dependencies and the Docker image. The bmap is copied next to the image after the image was written back.
  • The input bmap is the file given with the new --input-bmap argument (requires -b), else <image>.bmap or the bmap of the uncompressed image next to it (e.g. x.wic.bmap for x.wic.xz, as the Yocto build creates it). It is only read with -b. The command fails if the input bmap is for an image of another size, if a range does not match its sha256 checksum, or if the bmap has no range checksums. The new --no-input-bmap (requires -b) skips it, and the bmap then maps the whole image. The input bmap of an uncompressed image is checked before the image is copied.
  • The created bmap is named <output image>.bmap and written next to the output image, e.g. x.wic.bmap for x.wic and x.wic.xz.bmap for x.wic.xz, where bmaptool finds it. So with -p it no longer replaces the bmap of the input image, which stays unchanged.
  • The output image is the input image, for a compressed image without the compression extension, plus the -p extension, as before. The new -o/--output-image writes it to another path, and the input image stays unchanged. The -o name must have the extension of the -p compression, or no compression extension without -p. A command now fails instead of overwriting an existing file that is not the input image (a path given with -o is overwritten), replacing the bmap of an image that stays (e.g. x.wic.bmap for -i x.wic.xz without -p, also with --input-bmap or --no-input-bmap), or writing an image without -b while <output image>.bmap is next to it, or for a compressed output image the bmap of the uncompressed image, which bmaptool also uses. That bmap would no longer match. So for several commands on one image, use -b on each.
  • The output image and the bmap are written to a new file in the same directory and renamed over the old one, so a failed write, e.g. on a full disk, no longer destroys an image that is changed in place. A symlink is followed, and the old file keeps its permissions. The directory needs write access and space for one more image, and the new file belongs to the user who runs omnect-cli.
  • copy-from-image no longer writes the image back. It only reads, so an error can no longer truncate the source, and for x.wic.xz it no longer leaves x.wic next to it. It reads an uncompressed image directly, without a temp copy.
  • The partition files are written to their own temp dir. Before, they were named <num>.img next to the image, so an image named e.g. 1.img was overwritten by its partition 1. The temp dirs follow TMPDIR.
  • Partitions of a GPT with 4096-byte sectors are read at the right offset; before, the offsets were always counted in 512-byte sectors.
  • Known limitations, tracked as follow-up tasks: the created bmap maps every block that the image file stores as data, so for an image that is not sparse it maps the whole image, also with an input bmap. For a partition that does not start on a 4 KiB boundary, a block at the edge of each data run is written back, so the image is less sparse. In both cases the image content is correct.
  • Breaking: --generate-bmap-file is renamed to --output-bmap, so the two options read as a pair. The short option -b stays. The name conflicts above fail where they overwrote files before. -p bzip2 writes <image>.bz2 instead of <image>.bzip2, which bmaptool does not read.
  • roxmltree is added to read the bmap, and rustix for SEEK_DATA/SEEK_HOLE; libfs is removed. The test bmap testfiles/image.wic.bmap is now in git (*.bmap is in .gitignore), with checksums that match the test images. The version is bumped to 0.29.0, and ctutils to 0.4.5 for RUSTSEC-2026-0385.

Reason

A sparse image, e.g. one that xz decompressed into a file, or one after fallocate -d, has holes wherever the image is zero, and a bmap skips them. Some of these zeros must still be written: a bmap-based flash otherwise keeps what an earlier installation left there. An old ext4 journal can be replayed after a power cut and apply an old transaction to the new filesystem; old GPT entries break the entry array CRC; old FAT sectors and directory entries show up as used clusters or stale files. On an rpi4-64 image, mapping only the ext4 journals still drops 188 blocks of the Yocto bmap: GPT primary and backup entries, 69 blocks of boot, and 16–24 blocks on every ext4 partition.

The Yocto bmap maps all of these ranges, because the build image holds real data there. Keeping its ranges mapped covers every format without knowing about file systems. From a sparse image alone, omnect-cli cannot tell such zero blocks from free space, so without an input bmap it maps the whole image. bmaptool copy reads a mapped range with plain reads, so a hole in such a range is written as zeros: the zeros only have to be in the bmap, not allocated in the image file. This replaces the image-side approach discussed in omnect/meta-omnect#701.

With default-features = false, libfs made byte copies, so every image came back fully allocated, and the decompress wrote zero blocks as data. fallocate -d hid both. The sparse copy uses SEEK_DATA/SEEK_HOLE directly: libfs::next_sparse_segments seeks both files to the same offset, so it cannot read a partition out of the image, and libfs::copy_file copies each data segment with a single copy_file_range call, which moves at most 2 GiB, and ignores a short copy.

All omnect partitions have a fixed size, so builds of one machine have the same image size, and the bmap of another build (e.g. of an older download next to a new image) passes a size check. The sha256 of each range tells the builds apart; bmaptool copy runs the same check on flash.

omnect-cli derives every output name from the input name and finds the input bmap by name, so names can collide. omnect-cli runs in CI scripts, where nobody reads a warning, so each collision fails with an error that names the option to resolve it. A counter in the output name would not work: a script and the later bmaptool copy need a fixed name.

Writing only changed blocks also fixes a gap of the old dd conv=notrunc,sparse: a block that changed to all zeros was skipped and kept its old content; now it is written because it differs. In the same way, dd conv=sparse turned every zero block of a partition into a hole, so a zero block of a copied file that landed in free space stayed a hole in the image, and a bmap flash kept the old content there.

write_partition ran `fallocate -d` on the whole image, which turned every
zero-filled block into a hole. A bmap created afterwards with `-b` then
skipped those blocks, e.g. ext4 journals that the image deliberately
writes as zeros, so a bmap-based flash left the old journal data on the
device.

Write only the blocks that differ from the image instead. Unchanged
blocks keep their allocation, and a block that changed to all zeros is
now written too (`dd conv=sparse` skipped it and kept the old content).

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
- all direct dependencies to their latest version, omnect-crypto to
  0.5.0 (create_cert_and_key no longer takes the unused extensions
  argument; the extensions of a certificate without CSR are the same)
- actix-web without default features: the OAuth callback server only
  serves plain HTTP on localhost, and the http2 feature pulled in h2 0.3
  (RUSTSEC-2026-0258, no fixed 0.3 release)
- flate2 selects its rust_backend itself instead of relying on
  actix-web's compression features to enable one

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
libfs was built with `default-features = false`, which also drops its
`use_linux` feature. Without it libfs copies byte by byte, so every image
omnect-cli works on came back fully allocated, and a bmap created with `-b`
mapped the whole image. The `fallocate -d` that write_partition used to run
hid this.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
A fresh ext4 journal is all zeros, and a sparse image, e.g. one that
xz decompressed into a file, holds it as holes. A bmap then skips the
journal, so a bmap-based flash leaves the journal of the previous
installation on the device, and after a power cut its replay can apply
an old transaction to the new filesystem.

Before creating the bmap, rewrite the journal blocks of every ext4
partition with their own content. Holes become allocated zero blocks and
all other blocks stay unchanged. debugfs lists the journal blocks, so
the package now depends on e2fsprogs.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
@JanZachmann JanZachmann changed the title fix(file): keep the image layout when writing a partition back fix(file): keep holes and map ext4 journals in generated bmaps Sep 28, 2026
libfs::copy_file copies each data segment with one copy_file_range call and ignores a short copy, so a segment over 2 GiB was cut off without error. copy_image keeps the holes and loops until each segment is copied.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
- fall back to /usr/sbin/debugfs when debugfs is not in PATH
- reject an invalid s_log_block_size before it is used as a shift
- skip partitions whose superblock is past the end of the image
- skip the MBR extended container entry
- share SECTOR_SIZE between partition and functions

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
The deb package depends on e2fsprogs for debugfs.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Decompress wrote every zero block as data, so a bmap of a compressed input mapped the whole image. The decompressed file now seeks over zero blocks and keeps them as holes.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
The ext4 journal lookup is only used for bmap generation, so it moves with generate_bmap_file from functions.rs to bmap.rs.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
@JanZachmann JanZachmann changed the title fix(file): keep holes and map ext4 journals in generated bmaps fix(file): keep image holes and map ext4 journals in bmaps Sep 28, 2026
- a bmap flash onto a device with old content now overwrites the journal
- an invalid s_log_block_size is rejected
- a partition that starts past the end of the image is skipped
- the MBR extended container entry is not returned as a partition

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
…image-layout

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
@JanZachmann
JanZachmann marked this pull request as ready for review October 27, 2026 10:05
- return decoder errors at the end of the stream
- truncate the destination before a sparse decompress
- write runs of blocks in one call in SparseWriter
- find e2fsprogs tools in sbin, reuse get_partitions, share the image block size

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Sparse decompress turns every zero block into a hole, and a bmap created
from it skips them: the GPT entries, FAT sectors of boot and ext4
journals and metadata. A bmap-based flash then keeps what an earlier
installation left there.

Take the bmap of the input image (next to it, or --bmap) and write the
zero blocks of its ranges as data. Without an input bmap, every block is
written. This replaces the ext4 journal handling, so debugfs and
e2fsprogs are no longer needed.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
@JanZachmann JanZachmann changed the title fix(file): keep image holes and map ext4 journals in bmaps fix(file): keep image holes and keep the input bmap ranges mapped Oct 28, 2026
- widen bmap ranges to whole 4 KiB blocks, so a hole is not missed
  when the bmap block size is not a multiple of 4096
- copy-from-image does not read or enforce an input bmap
- read the input bmap before the image is copied, and name it in errors
- finish the compress stream, so a failed trailer write is an error
- revert the unused partition table split
- test a flash onto old content and a bmap of another image
- README: describe where the created bmap goes and when it maps the
  whole image

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
@JanZachmann JanZachmann changed the title fix(file): keep image holes and keep the input bmap ranges mapped fix(file): keep image holes and map the ranges of the input bmap Oct 28, 2026
The short option -b stays.

BREAKING CHANGE: --generate-bmap-file is now --output-bmap.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
@JanZachmann JanZachmann changed the title fix(file): keep image holes and map the ranges of the input bmap feat(file)!: keep image holes, map the input bmap ranges, rename bmap options Oct 28, 2026
@JanZachmann JanZachmann changed the title feat(file)!: keep image holes, map the input bmap ranges, rename bmap options fix(file)!: keep image holes, map the input bmap ranges, rename bmap options Oct 28, 2026
…no-input-bmap

omnect-cli derives the output image and bmap names from the input image.
A command now fails instead of overwriting an existing file that is not the
input image, replacing the input bmap of an image that stays (e.g.
x.wic.bmap for x.wic.xz), or leaving a bmap next to the output image that
no longer matches it. --output-image chooses the output path, and
--no-input-bmap skips a bmap next to the image that does not match.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
…copy

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Decompress with the multi-stream decoders, so an image made of several
streams (e.g. written by pbzip2) is read completely. Decompress straight
from the input image into the temp dir, without a copy of the compressed
file first.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Without -b, also fail when the bmap of the uncompressed image is next to a
compressed output image, because bmaptool falls back to it. With -b and an
output that is not the input image, also refuse to replace the bmap next to
the input image when --input-bmap or --no-input-bmap is given. An input bmap
that cannot be read gets the same hint as one that does not match.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
bmaptool does not read the .bzip2 extension, so it could not flash an
image written with -p bzip2.

BREAKING CHANGE: -p bzip2 writes <image>.bz2 instead of <image>.bzip2.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
The name given with -o must have the extension of the -p compression, or
no compression extension without -p, so the name matches the content.
For a compressed input, strip only the extension of its compression from
the derived output name.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
The write-back truncated the output image first, so a failed copy, e.g.
on a full disk, destroyed an image that was changed in place. Write the
image and the bmap to a new file in the same directory and rename it over
the old one. A symlink is followed, and the old file keeps its
permissions.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
The temp dir now follows TMPDIR and no longer needs its own tokio runtime
to be removed.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
The partition files were named <num>.img in the directory of the image,
so an image named e.g. 1.img was overwritten by its own partition 1.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
copy-from-image only reads the image, and the partition files no longer
go next to it, so a temp copy is needed only to decompress the image.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
gptman finds a GPT with 512 or 4096-byte sectors, but the partition
offsets were always counted in 512-byte sectors.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
@JanZachmann
JanZachmann requested a review from mlilien October 29, 2026 16:13
Comment thread src/file/output.rs Resolved
Comment thread src/file/compression.rs Resolved
Comment thread src/lib.rs Resolved
Comment thread src/file/bmap.rs Resolved
Comment thread src/lib.rs Resolved
Comment thread src/lib.rs Resolved
…pression

`-i x.wic.gz -p gzip` wrote `x.wic.gzip`, and `-i x.wic.bzip2 -p bzip2`
wrote `x.wic.bz2`, so a second run failed. gzip images now get `.gz`.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
bmaptool removes one extension of the image at a time until it finds a
bmap, so it also uses e.g. `x.bmap` for `x.wic`. The input bmap and the
stale bmap check now follow the same rule.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
…ename

The output image and its bmap are now staged first: written, synced on
the handle that wrote them, and given their permissions. Then the old
bmap is removed, the image and the bmap are renamed, and their
directories are synced. So a power loss cannot leave an empty or partial
image, and a failed bmap rename leaves no bmap that does not match the
new image. A compressed image is written into its staged file directly,
without a copy in TMPDIR.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
@JanZachmann
JanZachmann requested a review from mlilien October 29, 2026 17:05

@mlilien mlilien left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more points on 3da5a4d. The inline comments are the ones I would look at. The rest are optional nits:

  • README, Output image: /tmp also needs space for the uncompressed image plus partition copies, as it did before this PR. That is worth a sentence, since /tmp is often a small tmpfs and an xz image can expand to several GB.
  • lib.rs:86: for an uncompressed input with an input bmap, the mapped ranges are read once by verify_checksums and again by the copy to TMPDIR. Checking the checksums during the copy would save one pass. Low priority, because the pass in write_bmap is needed anyway (it hashes the modified image).
  • bmap.rs:118 / compression.rs:121: bmap_path and compressed_path both append .ext to the OsString. A shared append_extension(path, ext) helper would do.
  • Bmap::read: the root element, version and BmapFileChecksum are not checked. The range sha256 check still rejects a wrong bmap, so this is mainly about refusing a future major version (3.x), as bmaptool does.

Comment thread src/file/output.rs Resolved
Comment thread src/file/output.rs Resolved
Comment thread src/lib.rs Resolved
rename(2) to a mount point fails with EBUSY, so an in-place command on
an image file mounted into a container failed. If the kernel reports
the image as a mount root, its new content is staged in TMPDIR, because
the directory may not be writable then, and copied into the image. A
rename that fails with EBUSY or EXDEV also falls back to the copy.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
A rename needs write access only to the directory, so a read-only image
was replaced without an error. The command now fails before it starts
if the output image or its bmap exists and is not writable.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
write_bmap writes into any Write, so the bmap no longer goes through a
temp file, and nothing is done for it without -b.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
…d paths

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
…ersion

Like bmaptool, a bmap needs a version, and a major version above 2 is
refused.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
A mount point cannot be removed, so with an image and its bmap both
mounted into a container, -b failed before the image was written. The
copy into the bmap replaces its content anyway. Also clearer wording
for the mount point case and the TMPDIR space.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
rename(2) and unlink(2) need write access to the directory, which a
container often does not have when only the image is mounted. So a
detected mount point is not renamed to or removed, its new content is
copied into it.

Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
@JanZachmann

Copy link
Copy Markdown
Contributor Author

On the nits in the review of 3da5a4d:

  • README: 19bb6be describes the space TMPDIR needs: a copy of each partition the command reads or changes, and the uncompressed image for a command that changes an image or reads a compressed one.
  • append_extension: 9126d55 uses std Path::with_added_extension in bmap_path and compressed_path.
  • Bmap::read: 8f08735 checks the root element and refuses a missing version or a major version above 2, as bmaptool does.
  • lib.rs:86, checking the checksums during the copy: not changed. f0ead21 checks the input bmap on the source before the copy, so a wrong bmap fails before a multi-GB copy. Checking during the copy would move that failure after it. Also, the copy uses copy_file_range, which does not read the data through user space. Hashing during the copy would need that.

@JanZachmann
JanZachmann requested a review from mlilien November 3, 2026 20:00

@mlilien mlilien left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fixes. The three threads from the last round are resolved; I checked each against 6e9c9e1. Clippy (-D warnings) is clean, and unit and integration tests pass locally.

A few new points inline. One is outside the diff, so here:

src/lib.rs create_image_cert (around line 217): it writes the device certificate and private key under fixed names (device_cert_path.pem, device_key_path.key.pem, the edge_ca_* names) next to the input image and never deletes them. This is older than this PR, but -o makes "one base image, many device outputs" the documented workflow, so it matters more now:

  • Two jobs running identity set-device-certificate -i base.wic.xz -o devA.wic and ... -o devB.wic in the same workspace at the same time: B overwrites the files while A is still decompressing, so devA.wic can get B's certificate and key.
  • With a read-only input directory (-i /ro/base.wic.xz -o /out/dev.wic), the command fails at fs::write.
  • A device private key is left next to the base image, even when check_conflicts rejects the run.

Writing the cert/key into a TempDir would fix all three. Fine as a follow-up if you prefer.

Comment thread src/file/output.rs
.metadata()
.context(format!("cannot get the size of {}", self.file.display()))?
.len();
let dest = OpenOptions::new()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy_into_dest truncates the destination before copying. If sparse::copy_to_empty_file fails partway (I/O error, or no space because the host is btrfs with snapshots and truncating frees nothing), the image on the host is left truncated, and the finished image in TMPDIR is removed when Staged drops. Both old and new image are gone.

I know the copy isn't atomic, same as on main. But keeping the staged file on error (TempDir::keep() and logging its path) would be cheap and saves the result.

// a file with a zero range that spans whole image blocks
const ZERO_BLOCKS_FILE_EDGE: usize = 4096;
const ZERO_BLOCKS_FILE_ZEROS: usize = 64 * 1024;
const OTHER_IMAGE_BMAP: &str = "<bmap><ImageSize>4096</ImageSize><BlockSize>4096</BlockSize>\

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OTHER_IMAGE_BMAP has no version attribute and no ChecksumType. Since 8f08735, check_input_bmap_of_another_image and check_no_input_bmap fail on Bmap::read: no valid version and never reach the image-size check they are meant to test. With version="2.0" added it fails on no sha256 range checksums instead.

The tests still pass because they only match cannot use input bmap / --no-input-bmap. If check_layout broke, nothing would notice. Could the fixture be a valid bmap (version, ChecksumType, range checksums) with only the image size differing?

Comment thread src/file/output.rs
} else {
dir.to_path_buf()
};
let tmp_dir = tempfile::Builder::new()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The staged image now lives in a hidden .omnect-cli-* dir next to the output image, and TempDir only removes it on drop. On Ctrl-C or SIGTERM (e.g. a cancelled CI job) during the long compress/copy/fsync, no destructors run, and a multi-GB .omnect-cli-XXXX/image.wic.xz stays in the user's image directory. Before this PR, leftovers ended up in /tmp, which gets cleaned on reboot or is tmpfs.

Maybe a signal handler that removes the staging dir, or at least a note in the README.

Comment thread src/file/output.rs
let image = dir.path().join(IMAGE);
touch(&image);

assert!(is_mount_root(Path::new("/")));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This asserts that / is a mount root, which depends on the environment. In a plain chroot (pbuilder/sbuild style), / isn't a mount root, and kernels before 5.8 don't report STATX_ATTR_MOUNT_ROOT. Then is_mount_root returns false and the test fails, although the code works (it falls back to the EBUSY/EXDEV path). Maybe skip the assertion when STATX_ATTR_MOUNT_ROOT isn't in stx_attributes_mask.

Comment thread src/file/compression.rs
Compression::gzip => "gzip",
Compression::xz { .. } => "xz",
Compression::bzip2 => &["bz2", "bzip2"],
Compression::gzip => &["gz", "gzip"],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-p gzip now writes <image>.gz instead of <image>.gzip (the old extension() returned "gzip"). The Breaking section only mentions the bzip2 rename. A pipeline that uploads or flashes x.wic.gzip after -p gzip won't find the file after updating. Please add it to the breaking notes.

Comment thread src/file/output.rs
.context(format!("cannot set permissions of {}", file.display()))?;
}
// the handle that wrote the data also gets the write errors
handle

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Staged::write always calls sync_all on the staged file, also when the destination is a mount point and the file is only a TMPDIR copy that gets copied into the destination and deleted right after. With disk-backed TMPDIR, an in-place command on a bind-mounted 8 GiB image forces a full writeback of a throwaway file, and copy_into_dest then syncs the real destination anyway. The sync could be skipped on the copy path.

Comment thread src/file/output.rs
}
}

fn copy_into_dest(&self) -> Result<()> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy_into_dest repeats copy_image from lib.rs (open source, get length, sparse::copy_to_empty_file), and the uncompressed branch of temp_image (lib.rs:154) repeats sparse::copy_to_new_file. That makes three copies of the same sequence. Moving copy_image into file::sparse and calling it from here and from temp_image would keep later fixes (error context, permissions) in one place.

const OTHER_IMAGE_BMAP: &str = "<bmap><ImageSize>4096</ImageSize><BlockSize>4096</BlockSize>\
<BlockMap><Range>0</Range></BlockMap></bmap>";

fn bmap_path_of(image: &Path) -> PathBuf {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bmap_path_of duplicates the public omnect_cli::file::bmap::bmap_path, and four tests (lines 964, 988, 1035, 1092) still build the path inline with format!("{}.bmap", ...). If the naming rule changes, the tests keep the old one. Using bmap_path everywhere avoids that.

Comment thread project-context.md
- `src/image.rs` — firmware image architecture detection (ARM32/ARM64/x86_64)
- `src/file/mod.rs` — high-level image operations: identity config, certs, hostname patching
- `src/file/functions.rs` — partition read/modify/write via dd + e2cp/mcopy
- `src/file/functions.rs` — sparse partition read, modify via e2cp/mcopy, write back of changed blocks only

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module list adds bmap.rs and sparse.rs but not the new src/file/output.rs (output naming, conflict checks, staging, commit).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants