Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Layer Scanning] Preserve the file permissions of files written to disk when extracting from a tar file. Currently, all files had 0600 permissions, which breaks go binary extraction. #387

Merged
merged 1 commit into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 42 additions & 11 deletions artifact/image/layerscanning/image/file_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import (
"os"
"path"
"path/filepath"
"time"
)

const (

// filePermission represents the permission bits for a file, which are minimal since files in the
// layer scanning use case are read-only.
filePermission = 0600
Expand All @@ -31,28 +31,39 @@ const (
dirPermission = 0700
)

// FileNode represents a file in a virtual filesystem.
// fileNode represents a file in a virtual filesystem.
type fileNode struct {
// extractDir and originLayerID are used to construct the real file path of the fileNode.
extractDir string
originLayerID string
isWhiteout bool
virtualPath string
targetPath string
mode fs.FileMode
file *os.File

// isWhiteout is true if the fileNode represents a whiteout file
isWhiteout bool

// virtualPath is the path of the fileNode in the virtual filesystem.
virtualPath string
// targetPath is reserved for symlinks. It is the path that the symlink points to.
targetPath string

// size, mode, and modTime are used to implement the fs.FileInfo interface.
size int64
mode fs.FileMode
modTime time.Time

// file is the file object for the real file referred to by the fileNode.
file *os.File
}

// ========================================================
// fs.File METHODS
// ========================================================

// Stat returns the file info of real file referred by the fileNode.
// TODO: b/378130598 - Need to replace the os stat permission with the permissions on the filenode.
func (f *fileNode) Stat() (fs.FileInfo, error) {
if f.isWhiteout {
return nil, fs.ErrNotExist
}
return os.Stat(f.RealFilePath())
return f, nil
}

// Read reads the real file referred to by the fileNode.
Expand Down Expand Up @@ -103,13 +114,14 @@ func (f *fileNode) RealFilePath() string {
// fs.DirEntry METHODS
// ========================================================

// Name returns the name of the fileNode.
// Name returns the name of the fileNode. Name is also used to implement the fs.FileInfo interface.
func (f *fileNode) Name() string {
_, filename := path.Split(f.virtualPath)
return filename
}

// IsDir returns whether the fileNode represents a directory.
// IsDir returns whether the fileNode represents a directory. IsDir is also used to implement the
// fs.FileInfo interface.
func (f *fileNode) IsDir() bool {
return f.Type().IsDir()
}
Expand All @@ -123,3 +135,22 @@ func (f *fileNode) Type() fs.FileMode {
func (f *fileNode) Info() (fs.FileInfo, error) {
return f.Stat()
}

// ========================================================
// fs.FileInfo METHODS
// ========================================================
func (f *fileNode) Size() int64 {
return f.size
}

func (f *fileNode) Mode() fs.FileMode {
return f.mode
}

func (f *fileNode) ModTime() time.Time {
return f.modTime
}

func (f *fileNode) Sys() any {
return nil
}
90 changes: 89 additions & 1 deletion artifact/image/layerscanning/image/file_node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"path"
"path/filepath"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
Expand Down Expand Up @@ -59,7 +60,94 @@ var (

// TODO: b/377551664 - Add tests for the Stat method for the fileNode type.
func TestStat(t *testing.T) {
return
baseTime := time.Now()
regularFileNode := &fileNode{
extractDir: "tempDir",
originLayerID: "",
virtualPath: "/bar",
isWhiteout: false,
mode: filePermission,
size: 1,
modTime: baseTime,
}
symlinkFileNode := &fileNode{
extractDir: "tempDir",
originLayerID: "",
virtualPath: "/symlink-to-bar",
targetPath: "/bar",
isWhiteout: false,
mode: fs.ModeSymlink | filePermission,
size: 1,
modTime: baseTime,
}
whiteoutFileNode := &fileNode{
extractDir: "tempDir",
originLayerID: "",
virtualPath: "/bar",
isWhiteout: true,
mode: filePermission,
}

type info struct {
name string
size int64
mode fs.FileMode
modTime time.Time
}

tests := []struct {
name string
node *fileNode
wantInfo info
wantErr error
}{
{
name: "regular file",
node: regularFileNode,
wantInfo: info{
name: "bar",
size: 1,
mode: filePermission,
modTime: baseTime,
},
},
{
name: "symlink",
node: symlinkFileNode,
wantInfo: info{
name: "symlink-to-bar",
size: 1,
mode: fs.ModeSymlink | filePermission,
modTime: baseTime,
},
},
{
name: "whiteout file",
node: whiteoutFileNode,
wantErr: fs.ErrNotExist,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
gotFileNode, gotErr := tc.node.Stat()
if tc.wantErr != nil {
if diff := cmp.Diff(tc.wantErr, gotErr, cmpopts.EquateErrors()); diff != "" {
t.Errorf("Stat(%v) returned unexpected error (-want +got): %v", tc.node, diff)
}
return
}

gotInfo := info{
name: gotFileNode.Name(),
size: gotFileNode.Size(),
mode: gotFileNode.Mode(),
modTime: gotFileNode.ModTime(),
}
if diff := cmp.Diff(tc.wantInfo, gotInfo, cmp.AllowUnexported(info{})); diff != "" {
t.Errorf("Stat(%v) returned unexpected fileNode (-want +got): %v", tc.node, diff)
}
})
}
}

func TestRead(t *testing.T) {
Expand Down
13 changes: 11 additions & 2 deletions artifact/image/layerscanning/image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,12 +385,17 @@ func (img *Image) handleDir(realFilePath, virtualPath, originLayerID string, tar
return nil, fmt.Errorf("failed to create directory with realFilePath %s: %w", realFilePath, err)
}
}

fileInfo := header.FileInfo()

return &fileNode{
extractDir: img.ExtractDir,
originLayerID: originLayerID,
virtualPath: virtualPath,
isWhiteout: isWhiteout,
mode: fs.FileMode(header.Mode) | fs.ModeDir,
mode: fileInfo.Mode() | fs.ModeDir,
size: fileInfo.Size(),
modTime: fileInfo.ModTime(),
}, nil
}

Expand All @@ -415,12 +420,16 @@ func (img *Image) handleFile(realFilePath, virtualPath, originLayerID string, ta
return nil, fmt.Errorf("unable to copy file: %w", err)
}

fileInfo := header.FileInfo()

return &fileNode{
extractDir: img.ExtractDir,
originLayerID: originLayerID,
virtualPath: virtualPath,
isWhiteout: isWhiteout,
mode: fs.FileMode(header.Mode),
mode: fileInfo.Mode(),
size: fileInfo.Size(),
modTime: fileInfo.ModTime(),
}, nil
}

Expand Down
Loading