Skip to content

fix(image): truncate the output file of save and export - #5160

Merged
AkihiroSuda merged 1 commit into
containerd:mainfrom
ekalinin:fix/save-truncate-output
Aug 27, 2026
Merged

fix(image): truncate the output file of save and export#5160
AkihiroSuda merged 1 commit into
containerd:mainfrom
ekalinin:fix/save-truncate-output

Conversation

@ekalinin

Copy link
Copy Markdown
Contributor

nerdctl save -o FILE and nerdctl export -o FILE open the output with
os.O_CREATE|os.O_WRONLY and no os.O_TRUNC. Writing a smaller archive over a bigger one
therefore leaves the tail of the bigger one past the end of the new archive.

f, _ := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
f.Write([]byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) // 30 bytes, the larger image
f.Close()

f, _ = os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
f.Write([]byte("BBBBBBBBBB")) // 10 bytes, the smaller image
f.Close()
wrote 10 bytes, file holds 30: "BBBBBBBBBBAAAAAAAAAAAAAAAAAAAA"

This does not break nerdctl load: a tar reader stops at the end-of-archive marker and never
looks at what follows, which I checked with both archive/tar and the system tar. What is
wrong is the artifact — it is larger than the archive it is supposed to hold, and the extra
bytes belong to an unrelated image or container, which shows up in anything that checksums the
file, accounts for its size, or ships it somewhere.

docker save replaces the destination wholesale, writing through a temporary file and renaming
it (atomicwriter.New in docker/cli). Adding O_TRUNC gets nerdctl to the same observable
result without a new dependency: pkg/internal/filesystem is built for state files, with
locking and backups, not for streaming an archive that can be many gigabytes.

pkg/healthcheck/log.go opens a file with the same flags, but seeks to the end and appends on
purpose, so it is left alone.

Tests

TestSaveReplacesExistingFile and TestExportReplacesExistingFile write the bigger archive
first and the smaller one over it, then assert the file shrank. They compare against the size
measured before the overwrite rather than against a separately saved copy, so they do not depend
on two saves of the same image being byte-identical.

Both assert on the size rather than on load failing, since as above a reader would not notice
the tail.

Fixes #5159

`nerdctl save -o FILE` and `nerdctl export -o FILE` open the output with O_CREATE|O_WRONLY
and no O_TRUNC. Writing a smaller archive over a bigger one therefore leaves the tail of
the bigger one past the end of the new archive.

A tar reader stops at the end-of-archive marker, so `nerdctl load` reads such a file
without complaining. What is wrong is the artifact: it is larger than the archive it is
supposed to hold, and the extra bytes belong to an unrelated image or container, which
shows up in anything that checksums the file, accounts for its size, or ships it
somewhere. `docker save` replaces the destination wholesale, writing through a temporary
file and renaming it.

Add O_TRUNC, so that `-o` replaces the file as a shell redirect does.

`pkg/healthcheck/log.go` opens a file with the same flags, but seeks to the end and
appends on purpose, so it is left alone.

Fixes containerd#5159

Signed-off-by: Eugene Kalinin <e.v.kalinin@gmail.com>
@AkihiroSuda AkihiroSuda added this to the v2.4.0 milestone Aug 25, 2026

@AkihiroSuda AkihiroSuda left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks

@AkihiroSuda
AkihiroSuda merged commit d311c8f into containerd:main Aug 27, 2026
115 of 122 checks passed
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.

nerdctl save -o / export -o do not truncate an existing file

2 participants