Skip to content
Open
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*.out

# Dependency directories (remove the comment below to include it)
# vendor/
/vendor
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to ignore vendor in this PR?


# Go workspace file
go.work
Expand Down
68 changes: 65 additions & 3 deletions pkg/backend/build/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package build

import (
"archive/tar"
"bytes"
"context"
"encoding/json"
Expand All @@ -26,6 +27,7 @@
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"time"
Expand Down Expand Up @@ -123,12 +125,56 @@
}

func (ab *abstractBuilder) BuildLayer(ctx context.Context, mediaType, workDir, path string, hooks hooks.Hooks) (ocispec.Descriptor, error) {
logrus.Debugf("BuildLayer: mediaType %s, workDir %s, path %s", mediaType, workDir, path)
info, err := os.Stat(path)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to get file info: %w", err)
}

if info.IsDir() {
lInfo, err := os.Lstat(path)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to get file info: %w", err)
}
isSymlink := lInfo.Mode()&os.ModeSymlink != 0
if isSymlink {
// 对于符号链接,获取其指向的目标信息

Choose a reason for hiding this comment

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

medium

This comment is in Chinese. For consistency and maintainability of the codebase, please translate it to English.

Suggested change
// 对于符号链接,获取其指向的目标信息
// For a symbolic link, get its target information.

dstLinkPath, err := os.Readlink(path)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to read symlink: %w", err)
}

workDirPath, err := filepath.Abs(workDir)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to get absolute path of workDir: %w", err)
}

// Gets the relative path of the file as annotation.
//nolint:typecheck
relPath, err := filepath.Rel(workDirPath, path)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to get relative path: %w", err)
}
logrus.Debugf("builder: starting build layer for synlink %s, target path %s", relPath, dstLinkPath)

// build layer for the target of symbolic link.
reader := strings.NewReader("")
hash := sha256.New()
size, err := io.Copy(hash, reader)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to copy content to hash: %w", err)
}
digest := fmt.Sprintf("sha256:%x", hash.Sum(nil))
desc, err := ab.strategy.OutputLayer(ctx, mediaType, relPath, digest, size, reader, hooks)

Choose a reason for hiding this comment

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

medium

The reader is consumed by io.Copy on line 163 but is not reset before being passed to ab.strategy.OutputLayer. While this might work for an empty reader, it's fragile and inconsistent with computeDigestAndSize, which does reset the reader. Please reset the reader to its beginning before this line. Since a code suggestion cannot add lines, I'll describe the change. Before this line, please add:

if _, err := reader.Seek(0, io.SeekStart); err != nil {
    return ocispec.Descriptor{}, fmt.Errorf("failed to reset reader for symlink: %w", err)
}

if err != nil {
return desc, err
}
// Add file metadata to descriptor.
if err := addFileMetadata(&desc, path, relPath); err != nil {
return desc, err
}
return desc, nil
}
return ocispec.Descriptor{}, fmt.Errorf("%s is a directory and not supported yet", path)
}

Expand Down Expand Up @@ -432,11 +478,27 @@
// Set Typeflag.
switch {
case info.Mode().IsRegular():
metadata.Typeflag = 0 // Regular file
metadata.Typeflag = tar.TypeReg // Regular file
case info.Mode().IsDir():
metadata.Typeflag = 5 // Directory
metadata.Typeflag = tar.TypeDir // Directory
lInfo, err := os.Lstat(path)
if err != nil {
return metadata, fmt.Errorf("failed to get file info: %w", err)
}
isSymlink := lInfo.Mode()&os.ModeSymlink != 0
if isSymlink {
// 对于符号链接,获取其指向的目标信息
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be great if comment in English

dstLinkPath, err := os.Readlink(path)
if err != nil {
return metadata, fmt.Errorf("failed to read symlink: %w", err)
}
logrus.Debugf("builder: symlink detected for file %s -> %s", path, dstLinkPath)
metadata.Typeflag = tar.TypeSymlink // Symlink
metadata.DstLinkPath = dstLinkPath

Check failure on line 497 in pkg/backend/build/builder.go

View workflow job for this annotation

GitHub Actions / Test

metadata.DstLinkPath undefined (type "github.com/dragonflyoss/model-spec/specs-go/v1".FileMetadata has no field or method DstLinkPath)
}

case info.Mode()&os.ModeSymlink != 0:
metadata.Typeflag = 2 // Symlink
metadata.Typeflag = tar.TypeSymlink // Symlink
default:
return metadata, errors.New("unknown file typeflag")
}
Comment on lines 479 to 504

Choose a reason for hiding this comment

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

high

There are a few issues with the logic in getFileMetadata:

  1. Unreachable code: The case case info.Mode()&os.ModeSymlink != 0: on line 500 is unreachable. info comes from os.Stat, which follows symlinks, so info.Mode() will never have the os.ModeSymlink bit. This is a bug.
  2. Inefficiency: The os.Lstat call on line 484 is redundant. BuildLayer already calls os.Lstat on the same path. This information could be passed down to avoid a second system call.
  3. Incomplete symlink handling: The new logic correctly handles symlinks to directories. However, symlinks to files will fall into the case info.Mode().IsRegular() and be treated as regular files, not preserved as symlinks.

To fix this, getFileMetadata should be refactored to use os.Lstat at the beginning to determine the file type (including all symlinks) and then proceed accordingly. This would fix all three issues.

Expand Down
46 changes: 42 additions & 4 deletions pkg/backend/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@
package backend

import (
"archive/tar"
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"

legacymodelspec "github.com/dragonflyoss/model-spec/specs-go/v1"
modelspec "github.com/modelpack/model-spec/specs-go/v1"
Expand Down Expand Up @@ -114,22 +117,57 @@ func exportModelArtifact(ctx context.Context, store storage.Storage, manifest oc

// extractLayer extracts the layer to the output directory.
func extractLayer(desc ocispec.Descriptor, outputDir string, reader io.Reader) error {
var filepath string
var filePath string
if desc.Annotations != nil {
if desc.Annotations[modelspec.AnnotationFilepath] != "" {
filepath = desc.Annotations[modelspec.AnnotationFilepath]
filePath = desc.Annotations[modelspec.AnnotationFilepath]
} else {
filepath = desc.Annotations[legacymodelspec.AnnotationFilepath]
filePath = desc.Annotations[legacymodelspec.AnnotationFilepath]
}

}

// load symlink from annotation if exists
var fileMetadata *modelspec.FileMetadata
// Try to retrieve the file metadata from annotation for raw file.
if desc.Annotations != nil {
fileMetadataStr := desc.Annotations[modelspec.AnnotationFileMetadata]
if fileMetadataStr == "" {
fileMetadataStr = desc.Annotations[legacymodelspec.AnnotationFileMetadata]
}

if fileMetadataStr != "" {
if err := json.Unmarshal([]byte(fileMetadataStr), &fileMetadata); err != nil {
return err
}
}
}
if fileMetadata != nil {
if fileMetadata.Typeflag == tar.TypeSymlink && len(fileMetadata.DstLinkPath) > 0 {
// handle file metadata

dstFullPath := filepath.Join(outputDir, fileMetadata.DstLinkPath)
if err := os.MkdirAll(dstFullPath, 0755); err != nil && !os.IsExist(err) {
return fmt.Errorf("failed to create directory %s: %w", dstFullPath, err)
Comment on lines +149 to +151

Choose a reason for hiding this comment

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

security-critical critical

This block of code introduces a critical path traversal vulnerability. fileMetadata.DstLinkPath is not sanitized, and if it contains .. or is an absolute path, os.MkdirAll could write outside the intended outputDir.

Additionally, this function's responsibility should be to extract the artifact, which means creating the symlink. It should not be responsible for creating the symlink's target.

I strongly recommend removing lines 149-151. The os.Symlink call on line 157 is sufficient to create the symlink, and it will correctly be a dangling symlink if the target does not exist.

}
fullPath := filepath.Join(outputDir, filePath)
if err := os.Remove(fullPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove file %s: %w", fullPath, err)
}
if err := os.Symlink(fileMetadata.DstLinkPath, fullPath); err != nil {
return fmt.Errorf("failed to create symlink from %s to %s: %w", fullPath, dstFullPath, err)

Choose a reason for hiding this comment

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

medium

The error message is slightly confusing. dstFullPath is the resolved path of the symlink target, but the symlink itself contains the unresolved path fileMetadata.DstLinkPath. To make debugging easier, consider using fileMetadata.DstLinkPath in the error message to show what the symlink is pointing to.

Suggested change
return fmt.Errorf("failed to create symlink from %s to %s: %w", fullPath, dstFullPath, err)
return fmt.Errorf("failed to create symlink %s pointing to %s: %w", fullPath, fileMetadata.DstLinkPath, err)

}
logrus.Debugf("extract: created symlink from %s to %s", fullPath, dstFullPath)
return nil
}
}

codec, err := pkgcodec.New(pkgcodec.TypeFromMediaType(desc.MediaType))
if err != nil {
return fmt.Errorf("failed to create codec for media type %s: %w", desc.MediaType, err)
}

if err := codec.Decode(outputDir, filepath, reader, desc); err != nil {
if err := codec.Decode(outputDir, filePath, reader, desc); err != nil {
if errors.Is(err, pkgcodec.ErrAlreadyUpToDate) {
return err
}
Expand Down
Loading