mirror of
https://github.com/apple/container.git
synced 2026-09-14 19:55:43 +00:00
* Admit only valid OCI layout files when loading image. - Adds `AdmissionMapper` protocol for validating archive member paths and normalizing them to relative paths under the extraction root directory. - Renames `Reader.swift` to `ArchiveReader.swift` to match type name. - Rework `TempDir` to address `NSString.utf8String` deprecation warning in Swift 6.2.3. - Rework `ArchiveReader.extractContent()` to use an `AdmissionMapper` to validate and remap archive members before extracting. - Adds `IdentityAdmissionWrapper` for naive extraction. - Adds `NoSymlinkAdmissionWrapper` that only extracts regular files and directories under the extraction root. - Adds `OCIImageAdmissionWrapper` that only extracts valid OCI image layout paths. - Use `OCIImageAdmissionWrapper` for `cctl image load` and print rejected paths. * PR feedback. * Adds public init() for TrustedAdmissionMapper. * Replace AdmissionMapper with more secure extraction. - Adds FileDescriptor.mkdirSecure() to prevent root escapes on member pathnames, and to prevent symlink traversal. - Adds FileDescriptor.unlinkRecursive() to facilitate overwrites when there are multiple archive entries with the same member path. - Adds FileDescriptor.validateSymlinkTargetInRoot() to validate that extracted symlink targets do not escape the root. - Rewrite ArchiveReader.extractContents() to use secure path functions. * Remove unneeded symlink check, rename files. * Simplify the lexical normalizer workaround. * Reject member paths containing parent traversal components. * Remove unused lexical normalization workaround. * Fix leaking fds, extract absolute members as relative.
115 lines
4.6 KiB
Swift
115 lines
4.6 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
//
|
|
|
|
import ContainerizationArchive
|
|
import ContainerizationExtras
|
|
import ContainerizationOCI
|
|
import Foundation
|
|
import Testing
|
|
|
|
@testable import Containerization
|
|
|
|
@Suite
|
|
public class ImageStoreTests: ContainsAuth {
|
|
let store: ImageStore
|
|
let dir: URL
|
|
|
|
public init() {
|
|
let dir = FileManager.default.uniqueTemporaryDirectory(create: true)
|
|
let cs = try! LocalContentStore(path: dir)
|
|
let store = try! ImageStore(path: dir, contentStore: cs)
|
|
self.dir = dir
|
|
self.store = store
|
|
}
|
|
|
|
deinit {
|
|
try! FileManager.default.removeItem(at: self.dir)
|
|
}
|
|
|
|
@Test func testImageStoreOperation() async throws {
|
|
let fileManager = FileManager.default
|
|
let tempDir = fileManager.uniqueTemporaryDirectory()
|
|
defer {
|
|
try? fileManager.removeItem(at: tempDir)
|
|
}
|
|
|
|
let tarPath = Foundation.Bundle.module.url(forResource: "scratch", withExtension: "tar")!
|
|
let reader = try ArchiveReader(format: .pax, filter: .none, file: tarPath)
|
|
let rejectedPaths = try reader.extractContents(to: tempDir)
|
|
#expect(rejectedPaths.count == 0, "unexpected rejected paths [\(rejectedPaths)]")
|
|
|
|
let _ = try await self.store.load(from: tempDir)
|
|
let loaded = try await self.store.load(from: tempDir)
|
|
let expectedLoadedImage = "registry.local/integration-tests/scratch:latest"
|
|
#expect(loaded.first!.reference == "registry.local/integration-tests/scratch:latest")
|
|
|
|
guard let authentication = Self.authentication else {
|
|
return
|
|
}
|
|
let imageReference = "ghcr.io/apple/containerization/dockermanifestimage:0.0.2"
|
|
let busyboxImage = try await self.store.pull(reference: imageReference, auth: authentication)
|
|
|
|
let got = try await self.store.get(reference: imageReference)
|
|
#expect(got.descriptor == busyboxImage.descriptor)
|
|
|
|
let newTag = "registry.local/integration-tests/dockermanifestimage:latest"
|
|
let _ = try await self.store.tag(existing: imageReference, new: newTag)
|
|
|
|
let tempFile = self.dir.appending(path: "export.tar")
|
|
try await self.store.save(references: [imageReference, expectedLoadedImage], out: tempFile)
|
|
}
|
|
|
|
@Test(.disabled("External users cannot push images, disable while we find a better solution"))
|
|
func testImageStorePush() async throws {
|
|
guard let authentication = Self.authentication else {
|
|
return
|
|
}
|
|
let imageReference = "ghcr.io/apple/containerization/dockermanifestimage:0.0.2"
|
|
|
|
let remoteImageName = "ghcr.io/apple/test-images/image-push"
|
|
let epoch = Int(Date().timeIntervalSince1970.description)
|
|
let tag = epoch != nil ? String(epoch!) : "latest"
|
|
let upstreamTag = "\(remoteImageName):\(tag)"
|
|
let _ = try await self.store.tag(existing: imageReference, new: upstreamTag)
|
|
try await self.store.push(reference: upstreamTag, auth: authentication)
|
|
}
|
|
|
|
@Test func testLoadImageWithoutAnnotations() async throws {
|
|
let fileManager = FileManager.default
|
|
let tempDir = fileManager.uniqueTemporaryDirectory()
|
|
defer {
|
|
try? fileManager.removeItem(at: tempDir)
|
|
}
|
|
|
|
let tarPath = Foundation.Bundle.module.url(forResource: "scratch_no_annotations", withExtension: "tar")!
|
|
let reader = try ArchiveReader(format: .pax, filter: .none, file: tarPath)
|
|
let rejectedPaths = try reader.extractContents(to: tempDir)
|
|
#expect(rejectedPaths.count == 0, "unexpected rejected paths [\(rejectedPaths)]")
|
|
|
|
let loaded = try await self.store.load(from: tempDir)
|
|
|
|
#expect(loaded.count == 1)
|
|
|
|
let reference = loaded.first!.reference
|
|
#expect(reference.hasPrefix("untagged@sha256:"))
|
|
|
|
let retrieved = try await self.store.get(reference: reference)
|
|
#expect(retrieved.reference == reference)
|
|
}
|
|
}
|