updated vendor
This commit is contained in:
+87
-54
@@ -52,6 +52,16 @@ func (i *image) MediaType() (types.MediaType, error) {
|
||||
return i.base.MediaType()
|
||||
}
|
||||
|
||||
// isImageConfig reports whether the media type is a Docker or OCI image config.
|
||||
func isImageConfig(mt types.MediaType) bool {
|
||||
switch mt {
|
||||
case types.DockerConfigJSON, types.OCIConfigJSON:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (i *image) compute() error {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
@@ -60,6 +70,24 @@ func (i *image) compute() error {
|
||||
if i.computed {
|
||||
return nil
|
||||
}
|
||||
|
||||
m, err := i.base.Manifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifest := m.DeepCopy()
|
||||
|
||||
diffIDMap := make(map[v1.Hash]v1.Layer)
|
||||
digestMap := make(map[v1.Hash]v1.Layer)
|
||||
|
||||
// Determine effective config media type (user override takes precedence).
|
||||
cfgMediaType := manifest.Config.MediaType
|
||||
if i.configMediaType != nil {
|
||||
cfgMediaType = *i.configMediaType
|
||||
}
|
||||
|
||||
imageConfig := isImageConfig(cfgMediaType)
|
||||
|
||||
var configFile *v1.ConfigFile
|
||||
if i.configFile != nil {
|
||||
configFile = i.configFile
|
||||
@@ -70,33 +98,32 @@ func (i *image) compute() error {
|
||||
}
|
||||
configFile = cf.DeepCopy()
|
||||
}
|
||||
diffIDs := configFile.RootFS.DiffIDs
|
||||
history := configFile.History
|
||||
|
||||
diffIDMap := make(map[v1.Hash]v1.Layer)
|
||||
digestMap := make(map[v1.Hash]v1.Layer)
|
||||
// For image configs, update RootFS.DiffIDs and History from added layers.
|
||||
// For artifacts, skip this: the config has no rootfs or history fields.
|
||||
if imageConfig {
|
||||
diffIDs := configFile.RootFS.DiffIDs
|
||||
history := configFile.History
|
||||
|
||||
for _, add := range i.adds {
|
||||
history = append(history, add.History)
|
||||
if add.Layer != nil {
|
||||
diffID, err := add.Layer.DiffID()
|
||||
if err != nil {
|
||||
return err
|
||||
for _, add := range i.adds {
|
||||
history = append(history, add.History)
|
||||
if add.Layer != nil {
|
||||
diffID, err := add.Layer.DiffID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
diffIDs = append(diffIDs, diffID)
|
||||
diffIDMap[diffID] = add.Layer
|
||||
}
|
||||
diffIDs = append(diffIDs, diffID)
|
||||
diffIDMap[diffID] = add.Layer
|
||||
}
|
||||
|
||||
configFile.RootFS.DiffIDs = diffIDs
|
||||
configFile.History = history
|
||||
}
|
||||
|
||||
m, err := i.base.Manifest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifest := m.DeepCopy()
|
||||
manifestLayers := manifest.Layers
|
||||
for _, add := range i.adds {
|
||||
if add.Layer == nil {
|
||||
// Empty layers include only history in manifest.
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -105,14 +132,12 @@ func (i *image) compute() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fields in the addendum override the original descriptor.
|
||||
if len(add.Annotations) != 0 {
|
||||
desc.Annotations = add.Annotations
|
||||
}
|
||||
if len(add.URLs) != 0 {
|
||||
desc.URLs = add.URLs
|
||||
}
|
||||
|
||||
if add.MediaType != "" {
|
||||
desc.MediaType = add.MediaType
|
||||
}
|
||||
@@ -120,42 +145,38 @@ func (i *image) compute() error {
|
||||
manifestLayers = append(manifestLayers, *desc)
|
||||
digestMap[desc.Digest] = add.Layer
|
||||
}
|
||||
|
||||
configFile.RootFS.DiffIDs = diffIDs
|
||||
configFile.History = history
|
||||
|
||||
manifest.Layers = manifestLayers
|
||||
|
||||
rcfg, err := json.Marshal(configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d, sz, err := v1.SHA256(bytes.NewBuffer(rcfg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifest.Config.Digest = d
|
||||
manifest.Config.Size = sz
|
||||
// For image configs, re-marshal the config and update the manifest digest.
|
||||
// For artifacts, preserve the original config blob as-is to avoid
|
||||
// corrupting the digest via re-marshaling.
|
||||
if imageConfig {
|
||||
rcfg, err := json.Marshal(configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d, sz, err := v1.SHA256(bytes.NewBuffer(rcfg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifest.Config.Digest = d
|
||||
manifest.Config.Size = sz
|
||||
|
||||
// If Data was set in the base image, we need to update it in the mutated image.
|
||||
if m.Config.Data != nil {
|
||||
manifest.Config.Data = rcfg
|
||||
if m.Config.Data != nil {
|
||||
manifest.Config.Data = rcfg
|
||||
}
|
||||
}
|
||||
|
||||
// If the user wants to mutate the media type of the config
|
||||
if i.configMediaType != nil {
|
||||
manifest.Config.MediaType = *i.configMediaType
|
||||
}
|
||||
|
||||
if i.mediaType != nil {
|
||||
manifest.MediaType = *i.mediaType
|
||||
}
|
||||
|
||||
if i.annotations != nil {
|
||||
if manifest.Annotations == nil {
|
||||
manifest.Annotations = map[string]string{}
|
||||
}
|
||||
|
||||
for k, v := range i.annotations {
|
||||
manifest.Annotations[k] = v
|
||||
}
|
||||
@@ -173,29 +194,34 @@ func (i *image) compute() error {
|
||||
// Layers returns the ordered collection of filesystem layers that comprise this image.
|
||||
// The order of the list is oldest/base layer first, and most-recent/top layer last.
|
||||
func (i *image) Layers() ([]v1.Layer, error) {
|
||||
if err := i.compute(); errors.Is(err, stream.ErrNotComputed) {
|
||||
// Image contains a streamable layer which has not yet been
|
||||
// consumed. Just return the layers we have in case the caller
|
||||
// is going to consume the layers.
|
||||
if err := i.compute(); errors.Is(err, stream.ErrNotComputed) || (i.manifest != nil && !isImageConfig(i.manifest.Config.MediaType)) {
|
||||
// Stream not yet consumed, or non-image OCI artifact (RootFS.DiffIDs
|
||||
// is empty so partial.DiffIDs returns nothing). Fall back to the base
|
||||
// layers plus any added layers.
|
||||
layers, err := i.base.Layers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, add := range i.adds {
|
||||
layers = append(layers, add.Layer)
|
||||
if add.Layer != nil {
|
||||
layers = append(layers, add.Layer)
|
||||
}
|
||||
}
|
||||
return layers, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
diffIDs, err := partial.DiffIDs(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ls := make([]v1.Layer, 0, len(diffIDs))
|
||||
for _, h := range diffIDs {
|
||||
l, err := i.LayerByDiffID(h)
|
||||
// Walk manifest layer descriptors by digest rather than rootfs diff
|
||||
// IDs. Two layers can legitimately share a diff ID — same uncompressed
|
||||
// content, different compression — and produce distinct digests. The
|
||||
// manifest preserves the per-occurrence digest; LayerByDiffID does not,
|
||||
// which previously caused duplicate-diff-ID layers to collapse to a
|
||||
// single entry in the returned slice and break blob upload for
|
||||
// downstream pushers (see #2034).
|
||||
ls := make([]v1.Layer, 0, len(i.manifest.Layers))
|
||||
for _, desc := range i.manifest.Layers {
|
||||
l, err := i.LayerByDigest(desc.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -220,11 +246,18 @@ func (i *image) ConfigFile() (*v1.ConfigFile, error) {
|
||||
return i.configFile.DeepCopy(), nil
|
||||
}
|
||||
|
||||
// RawConfigFile returns the serialized bytes of ConfigFile()
|
||||
// RawConfigFile returns the serialized bytes of ConfigFile().
|
||||
// For non-image OCI artifacts, returns the original raw config to preserve
|
||||
// the config blob digest.
|
||||
func (i *image) RawConfigFile() ([]byte, error) {
|
||||
if err := i.compute(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If the manifest config is not a standard image config, return the
|
||||
// original raw bytes to avoid corrupting the digest via re-marshaling.
|
||||
if i.manifest != nil && !isImageConfig(i.manifest.Config.MediaType) {
|
||||
return i.base.RawConfigFile()
|
||||
}
|
||||
return json.Marshal(i.configFile)
|
||||
}
|
||||
|
||||
|
||||
+73
-72
@@ -277,90 +277,91 @@ func extract(img v1.Image, w io.Writer) error {
|
||||
// whiteout layers more efficient, since we can just keep track of the removed
|
||||
// files as we see .wh. layers and ignore those in previous layers.
|
||||
for i := len(layers) - 1; i >= 0; i-- {
|
||||
layer := layers[i]
|
||||
layerReader, err := layer.Uncompressed()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading layer contents: %w", err)
|
||||
if err := extractLayer(tarWriter, fileMap, layers[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
defer layerReader.Close()
|
||||
tarReader := tar.NewReader(layerReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading tar: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Some tools prepend everything with "./", so if we don't Clean the
|
||||
// name, we may have duplicate entries, which angers tar-split.
|
||||
header.Name = filepath.Clean(header.Name)
|
||||
func extractLayer(tarWriter *tar.Writer, fileMap map[string]bool, layer v1.Layer) error {
|
||||
layerReader, err := layer.Uncompressed()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading layer contents: %w", err)
|
||||
}
|
||||
defer layerReader.Close()
|
||||
|
||||
// Normalize absolute paths to relative to prevent writing outside
|
||||
// the extraction root (Zip Slip / CVE-2018-15664 class).
|
||||
// Many OCI tools emit absolute paths; stripping the leading slash
|
||||
// preserves the entry while removing the danger.
|
||||
if filepath.IsAbs(header.Name) {
|
||||
header.Name = strings.TrimLeft(header.Name, "/")
|
||||
}
|
||||
// After normalization, reject any remaining path traversal.
|
||||
if strings.HasPrefix(header.Name, "..") {
|
||||
continue
|
||||
}
|
||||
tarReader := tar.NewReader(layerReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading tar: %w", err)
|
||||
}
|
||||
|
||||
// Reject symlinks and hardlinks that point outside the extraction
|
||||
// root. An attacker can create a symlink to /etc and then write
|
||||
// files through it in a subsequent layer entry.
|
||||
if header.Typeflag == tar.TypeSymlink || header.Typeflag == tar.TypeLink {
|
||||
linkTarget := filepath.Clean(header.Linkname)
|
||||
if strings.HasPrefix(linkTarget, "..") || filepath.IsAbs(linkTarget) {
|
||||
// Some tools prepend everything with "./", so if we don't Clean the
|
||||
// name, we may have duplicate entries, which angers tar-split.
|
||||
header.Name = filepath.Clean(header.Name)
|
||||
|
||||
// Reject relative symlinks and hardlinks whose targets escape the
|
||||
// image rootfs. Relative targets are resolved against the symlink's
|
||||
// own directory: if the clean result starts with ".." the link would
|
||||
// leave the rootfs. Relative symlinks that stay within the rootfs
|
||||
// (common for glibc, C toolchains, etc.) are preserved unchanged.
|
||||
// Absolute targets are left as-is; see #2238 for ongoing discussion
|
||||
// on whether they should be pruned.
|
||||
if header.Typeflag == tar.TypeSymlink || header.Typeflag == tar.TypeLink {
|
||||
if !filepath.IsAbs(header.Linkname) {
|
||||
resolved := filepath.Clean(filepath.Join(filepath.Dir(header.Name), header.Linkname)) //nolint:gosec // G305: path is only used for validation, not file I/O
|
||||
if strings.HasPrefix(resolved, "..") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// force PAX format to remove Name/Linkname length limit of 100 characters
|
||||
// required by USTAR and to not depend on internal tar package guess which
|
||||
// prefers USTAR over PAX
|
||||
header.Format = tar.FormatPAX
|
||||
// force PAX format to remove Name/Linkname length limit of 100 characters
|
||||
// required by USTAR and to not depend on internal tar package guess which
|
||||
// prefers USTAR over PAX
|
||||
header.Format = tar.FormatPAX
|
||||
|
||||
basename := filepath.Base(header.Name)
|
||||
dirname := filepath.Dir(header.Name)
|
||||
tombstone := strings.HasPrefix(basename, whiteoutPrefix)
|
||||
if tombstone {
|
||||
basename = basename[len(whiteoutPrefix):]
|
||||
basename := filepath.Base(header.Name)
|
||||
dirname := filepath.Dir(header.Name)
|
||||
tombstone := strings.HasPrefix(basename, whiteoutPrefix)
|
||||
if tombstone {
|
||||
basename = basename[len(whiteoutPrefix):]
|
||||
}
|
||||
|
||||
// check if we have seen value before
|
||||
// if we're checking a directory, don't filepath.Join names
|
||||
var name string
|
||||
if header.Typeflag == tar.TypeDir {
|
||||
name = header.Name
|
||||
} else {
|
||||
name = filepath.Join(dirname, basename)
|
||||
}
|
||||
|
||||
if _, ok := fileMap[name]; ok && !tombstone {
|
||||
continue
|
||||
}
|
||||
|
||||
// check for a whited out parent directory
|
||||
if inWhiteoutDir(fileMap, name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// mark file as handled. non-directory implicitly tombstones
|
||||
// any entries with a matching (or child) name
|
||||
fileMap[name] = tombstone || (header.Typeflag != tar.TypeDir)
|
||||
if !tombstone {
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if we have seen value before
|
||||
// if we're checking a directory, don't filepath.Join names
|
||||
var name string
|
||||
if header.Typeflag == tar.TypeDir {
|
||||
name = header.Name
|
||||
} else {
|
||||
name = filepath.Join(dirname, basename)
|
||||
}
|
||||
|
||||
if _, ok := fileMap[name]; ok && !tombstone {
|
||||
continue
|
||||
}
|
||||
|
||||
// check for a whited out parent directory
|
||||
if inWhiteoutDir(fileMap, name) {
|
||||
continue
|
||||
}
|
||||
|
||||
// mark file as handled. non-directory implicitly tombstones
|
||||
// any entries with a matching (or child) name
|
||||
fileMap[name] = tombstone || (header.Typeflag != tar.TypeDir)
|
||||
if !tombstone {
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
if header.Size > 0 {
|
||||
if _, err := io.CopyN(tarWriter, tarReader, header.Size); err != nil {
|
||||
return err
|
||||
}
|
||||
if header.Size > 0 {
|
||||
if _, err := io.CopyN(tarWriter, tarReader, header.Size); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user