-
Notifications
You must be signed in to change notification settings - Fork 21
fix: build image layer for dir symlink #350
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
| package build | ||
|
|
||
| import ( | ||
| "archive/tar" | ||
| "bytes" | ||
| "context" | ||
| "encoding/json" | ||
|
|
@@ -26,6 +27,7 @@ | |
| "os" | ||
| "path/filepath" | ||
| "strconv" | ||
| "strings" | ||
| "sync" | ||
| "syscall" | ||
| "time" | ||
|
|
@@ -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 { | ||
| // 对于符号链接,获取其指向的目标信息 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 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) | ||
| } | ||
|
|
||
|
|
@@ -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 { | ||
| // 对于符号链接,获取其指向的目标信息 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
|
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are a few issues with the logic in
To fix this, |
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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" | ||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block of code introduces a critical path traversal vulnerability. 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 |
||||||
| } | ||||||
| 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) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error message is slightly confusing.
Suggested change
|
||||||
| } | ||||||
| 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 | ||||||
| } | ||||||
|
|
||||||
There was a problem hiding this comment.
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?