Skip to content

Commit 39fc1cb

Browse files
committed
pkg/archive: split tar writing in prepareAddFile() and addFile()
This allows disinguishing between inherent fatal error (writing to the tar archive failed, eg. disk full) and non-fatal error (tarWithOptionsTo is racing with another process mutating the file system.) Signed-off-by: Han-Wen Nienhuys <[email protected]>
1 parent 16d5eaa commit 39fc1cb

File tree

3 files changed

+115
-37
lines changed

3 files changed

+115
-37
lines changed

pkg/archive/archive.go

+65-36
Original file line numberDiff line numberDiff line change
@@ -528,38 +528,56 @@ func canonicalTarName(name string, isDir bool) (string, error) {
528528
return name, nil
529529
}
530530

531-
// addFile adds a file from `path` as `name` to the tar archive.
532-
func (ta *tarWriter) addFile(path, name string) error {
531+
type addFileData struct {
532+
// The path from which to read contents.
533+
path string
534+
535+
// os.Stat for the above.
536+
fi os.FileInfo
537+
538+
// The file header of the above.
539+
hdr *tar.Header
540+
541+
// if present, an extra whiteout entry to write after the header.
542+
extraWhiteout *tar.Header
543+
}
544+
545+
// prepareAddFile generates the tar file header(s) for adding a file
546+
// from path as name to the tar archive, without writing to the
547+
// tar stream. Thus, any error may be ignored without corrupting the
548+
// tar file. A (nil, nil) return means that the file should be
549+
// ignored for non-error reasons.
550+
func (ta *tarWriter) prepareAddFile(path, name string) (*addFileData, error) {
533551
fi, err := os.Lstat(path)
534552
if err != nil {
535-
return err
553+
return nil, err
536554
}
537555

538556
var link string
539557
if fi.Mode()&os.ModeSymlink != 0 {
540558
var err error
541559
link, err = os.Readlink(path)
542560
if err != nil {
543-
return err
561+
return nil, err
544562
}
545563
}
546564
if fi.Mode()&os.ModeSocket != 0 {
547565
logrus.Infof("archive: skipping %q since it is a socket", path)
548-
return nil
566+
return nil, nil
549567
}
550568

551569
hdr, err := FileInfoHeader(name, fi, link)
552570
if err != nil {
553-
return err
571+
return nil, err
554572
}
555573
if err := readSecurityXattrToTarHeader(path, hdr); err != nil {
556-
return err
574+
return nil, err
557575
}
558576
if err := readUserXattrToTarHeader(path, hdr); err != nil {
559-
return err
577+
return nil, err
560578
}
561579
if err := ReadFileFlagsToTarHeader(path, hdr); err != nil {
562-
return err
580+
return nil, err
563581
}
564582
if ta.CopyPass {
565583
copyPassHeader(hdr)
@@ -584,11 +602,11 @@ func (ta *tarWriter) addFile(path, name string) error {
584602
if !strings.HasPrefix(filepath.Base(hdr.Name), WhiteoutPrefix) && !ta.IDMappings.Empty() {
585603
fileIDPair, err := getFileUIDGID(fi.Sys())
586604
if err != nil {
587-
return err
605+
return nil, err
588606
}
589607
hdr.Uid, hdr.Gid, err = ta.IDMappings.ToContainer(fileIDPair)
590608
if err != nil {
591-
return err
609+
return nil, err
592610
}
593611
}
594612

@@ -611,6 +629,11 @@ func (ta *tarWriter) addFile(path, name string) error {
611629

612630
maybeTruncateHeaderModTime(hdr)
613631

632+
result := &addFileData{
633+
path: path,
634+
hdr: hdr,
635+
fi: fi,
636+
}
614637
if ta.WhiteoutConverter != nil {
615638
// The WhiteoutConverter suggests a generic mechanism,
616639
// but this code is only used to convert between
@@ -621,36 +644,41 @@ func (ta *tarWriter) addFile(path, name string) error {
621644
//
622645
// For AUFS, a directory with all its contents deleted
623646
// should be represented as a directory containing a
624-
// magic whiteout regular file, hence the extra wo header
625-
// returned here.
626-
//
627-
// We assume that whiteout entries are empty;
628-
// otherwise, if we write hdr with hdr.Size > 0, we
629-
// have to write the body before we can write the `wo`
630-
// header.
631-
wo, err := ta.WhiteoutConverter.ConvertWrite(hdr, path, fi)
647+
// magic whiteout empty regular file, hence the
648+
// extraWhiteout header returned here.
649+
result.extraWhiteout, err = ta.WhiteoutConverter.ConvertWrite(hdr, path, fi)
632650
if err != nil {
633-
return err
651+
return nil, err
634652
}
653+
}
635654

636-
if wo != nil {
637-
if hdr.Typeflag != tar.TypeReg || hdr.Size == 0 {
638-
if err := ta.TarWriter.WriteHeader(hdr); err != nil {
639-
return err
640-
}
641-
hdr = wo
642-
} else {
643-
logrus.Infof("tar: cannot use whiteout for non-empty file %s", hdr.Name)
644-
}
655+
return result, nil
656+
}
657+
658+
// addFile performs the write. An error here corrupts the tar file.
659+
func (ta *tarWriter) addFile(headers *addFileData) error {
660+
hdr := headers.hdr
661+
if headers.extraWhiteout != nil {
662+
if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 {
663+
// If we write hdr with hdr.Size > 0, we have
664+
// to write the body before we can write the
665+
// extraWhiteout header. This can only happen
666+
// if the contract for WhiteoutConverter is
667+
// not honored, so bail out.
668+
return fmt.Errorf("tar: cannot use extra whiteout with non-empty file %s", hdr.Name)
669+
}
670+
if err := ta.TarWriter.WriteHeader(hdr); err != nil {
671+
return err
645672
}
673+
hdr = headers.extraWhiteout
646674
}
647675

648676
if err := ta.TarWriter.WriteHeader(hdr); err != nil {
649677
return err
650678
}
651679

652680
if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 {
653-
file, err := os.Open(path)
681+
file, err := os.Open(headers.path)
654682
if err != nil {
655683
return err
656684
}
@@ -668,8 +696,8 @@ func (ta *tarWriter) addFile(path, name string) error {
668696
}
669697
}
670698

671-
if !fi.IsDir() && hasHardlinks(fi) {
672-
ta.SeenFiles[getInodeFromStat(fi.Sys())] = hdr.Name
699+
if !headers.fi.IsDir() && hasHardlinks(headers.fi) {
700+
ta.SeenFiles[getInodeFromStat(headers.fi.Sys())] = headers.hdr.Name
673701
}
674702

675703
return nil
@@ -1024,10 +1052,11 @@ func tarWithOptionsTo(dest io.WriteCloser, srcPath string, options *TarOptions)
10241052
relFilePath = strings.Replace(relFilePath, include, replacement, 1)
10251053
}
10261054

1027-
if err := ta.addFile(filePath, relFilePath); err != nil {
1028-
logrus.Errorf("Can't add file %s to tar: %s", filePath, err)
1029-
// if pipe is broken, stop writing tar stream to it
1030-
if err == io.ErrClosedPipe {
1055+
headers, err := ta.prepareAddFile(filePath, relFilePath)
1056+
if err != nil {
1057+
logrus.Errorf("Can't add file %s to tar: %s; skipping", filePath, err)
1058+
} else if headers != nil {
1059+
if err := ta.addFile(headers); err != nil {
10311060
return err
10321061
}
10331062
}

pkg/archive/archive_test.go

+43
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package archive
33
import (
44
"archive/tar"
55
"bytes"
6+
"errors"
67
"fmt"
78
"io"
89
"net"
@@ -1266,3 +1267,45 @@ func TestTimestamp(t *testing.T) {
12661267
// we expect the ones with a fixed timestamp to be the same
12671268
assert.Equal(t, origTarEpochOptions, laterTarEpochOptions)
12681269
}
1270+
1271+
type errorBuf struct {
1272+
bytes.Buffer
1273+
1274+
err error
1275+
failWrite int
1276+
writeCount int
1277+
}
1278+
1279+
func (b *errorBuf) Write(d []byte) (int, error) {
1280+
b.writeCount++
1281+
if b.failWrite == b.writeCount {
1282+
return 0, b.err
1283+
}
1284+
return b.Buffer.Write(d)
1285+
}
1286+
1287+
func (b *errorBuf) Close() error {
1288+
return nil
1289+
}
1290+
1291+
func TestTarErrorHandling(t *testing.T) {
1292+
dir := t.TempDir()
1293+
1294+
for i := range 10 {
1295+
if err := os.WriteFile(filepath.Join(dir, fmt.Sprintf("file%d", i)), bytes.Repeat([]byte("hello"), 32<<10),
1296+
0o700); err != nil {
1297+
t.Fatal(err)
1298+
}
1299+
}
1300+
1301+
dest := &errorBuf{
1302+
failWrite: 1,
1303+
err: errors.New("boom"),
1304+
}
1305+
1306+
if err := tarWithOptionsTo(dest, dir, &TarOptions{
1307+
Compression: Uncompressed,
1308+
}); !errors.Is(err, dest.err) {
1309+
t.Fatalf("Did not propagate error; got %v", err)
1310+
}
1311+
}

pkg/archive/changes.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -481,8 +481,14 @@ func ExportChanges(dir string, changes []Change, uidMaps, gidMaps []idtools.IDMa
481481
}
482482
} else {
483483
path := filepath.Join(dir, change.Path)
484-
if err := ta.addFile(path, change.Path[1:]); err != nil {
484+
headers, err := ta.prepareAddFile(path, change.Path[1:])
485+
if err != nil {
485486
logrus.Debugf("Can't add file %s to tar: %s", path, err)
487+
} else if headers != nil {
488+
if err := ta.addFile(headers); err != nil {
489+
writer.CloseWithError(err)
490+
return
491+
}
486492
}
487493
}
488494
}

0 commit comments

Comments
 (0)