Description
nerdctl save -o FILE and nerdctl export -o FILE open the output with os.O_CREATE|os.O_WRONLY and no os.O_TRUNC. When FILE already exists and is longer than what is about to be written, the tail of the previous content survives past the end of the new archive.
docker save does not have this problem: it writes through an atomic writer (temp file plus rename), so the destination is replaced wholesale.
// docker/cli cli/command/image/save.go
writer, err := atomicwriter.New(opts.output, 0o600)
Steps to reproduce the issue
nerdctl save -o out.tar <a larger image>
nerdctl save -o out.tar <a smaller image>
ls -l out.tar — it still has the size of the archive from step 1.
The same applies to nerdctl export -o out.tar <container>.
The open flags in isolation:
func open(path string) (*os.File, error) {
// as in image_save.go and container_export.go
return os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
}
f, _ := open(path)
f.Write([]byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) // 30 bytes, the larger image
f.Close()
f, _ = open(path)
f.Write([]byte("BBBBBBBBBB")) // 10 bytes, the smaller image
f.Close()
wrote 10 bytes, file holds 30: "BBBBBBBBBBAAAAAAAAAAAAAAAAAAAA"
Describe the results you received and expected
Received: the file keeps the size of the previous, larger archive, with the leftover bytes of that archive sitting after the end of the new one.
Note that 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 itself — it is larger than the archive it is supposed to contain, and the extra bytes belong to an unrelated image, which shows up in anything that checksums the file, accounts for its size, or ships it somewhere.
Expected: -o replaces the file, as docker save -o and docker export -o do, and 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 not affected.
What version of nerdctl are you using?
main, at c235f00.
The flags came in with 4b8061d (2024-08-30) for save and 3e50703 (2025-08-05) for export.
Description
nerdctl save -o FILEandnerdctl export -o FILEopen the output withos.O_CREATE|os.O_WRONLYand noos.O_TRUNC. WhenFILEalready exists and is longer than what is about to be written, the tail of the previous content survives past the end of the new archive.docker savedoes not have this problem: it writes through an atomic writer (temp file plus rename), so the destination is replaced wholesale.Steps to reproduce the issue
nerdctl save -o out.tar <a larger image>nerdctl save -o out.tar <a smaller image>ls -l out.tar— it still has the size of the archive from step 1.The same applies to
nerdctl export -o out.tar <container>.The open flags in isolation:
wrote 10 bytes, file holds 30: "BBBBBBBBBBAAAAAAAAAAAAAAAAAAAA"Describe the results you received and expected
Received: the file keeps the size of the previous, larger archive, with the leftover bytes of that archive sitting after the end of the new one.
Note that 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 botharchive/tarand the systemtar. What is wrong is the artifact itself — it is larger than the archive it is supposed to contain, and the extra bytes belong to an unrelated image, which shows up in anything that checksums the file, accounts for its size, or ships it somewhere.Expected:
-oreplaces the file, asdocker save -oanddocker export -odo, and as a shell redirect does.pkg/healthcheck/log.goopens a file with the same flags, but seeks to the end and appends on purpose, so it is not affected.What version of nerdctl are you using?
main, at c235f00.
The flags came in with 4b8061d (2024-08-30) for
saveand 3e50703 (2025-08-05) forexport.