Verify kernel archive integrity (#1703)

Closes https://github.com/apple/container/issues/1687

The default kernel archive is downloaded from a remote release URL
during first-run setup and via `container system kernel set
--recommended`. Previously, the archive contents were not verified after
download, so integrity depended on HTTPS and the release artifact
remaining unchanged.

This change adds digest verification for kernel archives. The
recommended/default kernel now has pinned digest metadata using an
algorithm-prefixed value such as `sha256:<hex>`. `container system
kernel set --tar` accepts `--digest`; remote tar URLs require it, and
local tar archives can also be verified before unpacking and
installation.

The system config also supports `kernel.digest`, and a custom
`kernel.url` must provide a digest for that archive.
This commit is contained in:
0x4C33
2026-07-13 13:04:39 -07:00
committed by GitHub
parent 5f277a9cbf
commit 57b07fa2ac
15 changed files with 460 additions and 45 deletions
+1
View File
@@ -211,6 +211,7 @@ let package = Package(
name: "ContainerAPIServiceTests",
dependencies: [
.product(name: "Containerization", package: "containerization"),
"ContainerAPIService",
"ContainerResource",
"ContainerRuntimeLinuxClient",
"ContainerRuntimeClient",
@@ -47,18 +47,25 @@ extension Application {
@Option(name: .customLong("tar"), help: "Filesystem path or remote URL to a tar archive containing a kernel file")
var tarPath: String? = nil
@Option(name: .long, help: "Expected digest for the tar archive, for example sha256:<hex>. Required when --tar is a remote URL.")
var digest: String? = nil
@OptionGroup
public var logOptions: Flags.Logging
public init() {}
public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try await Application.loadContainerSystemConfig()
if recommended {
let containerSystemConfig: ContainerSystemConfig = try await Application.loadContainerSystemConfig()
let url = containerSystemConfig.kernel.url
let path: String = containerSystemConfig.kernel.binaryPath
log.info("Installing the recommended kernel from \(url)...")
try await Self.downloadAndInstallWithProgressBar(tarRemoteURL: url, kernelFilePath: path, force: force)
try await Self.downloadAndInstallWithProgressBar(
tarRemoteURL: url,
kernelFilePath: path,
expectedDigest: containerSystemConfig.kernel.digest,
force: force)
return
}
guard tarPath != nil else {
@@ -68,6 +75,9 @@ extension Application {
}
private func setKernelFromBinary() async throws {
guard digest == nil else {
throw ArgumentParser.ValidationError("'--digest' can only be used with '--tar'")
}
guard let binaryPath else {
throw ArgumentParser.ValidationError("missing argument '--binary'")
}
@@ -84,16 +94,32 @@ extension Application {
throw ArgumentParser.ValidationError("missing argument '--tar")
}
let platform = try getSystemPlatform()
let remoteURL = URL(string: tarPath)
let remoteScheme = remoteURL?.scheme?.lowercased()
let isHTTPURL = remoteScheme == "http" || remoteScheme == "https"
let localTarPath = URL(fileURLWithPath: tarPath, relativeTo: .currentDirectory()).path
let fm = FileManager.default
if fm.fileExists(atPath: localTarPath) {
try await ClientKernel.installKernelFromTar(tarFile: localTarPath, kernelFilePath: binaryPath, platform: platform, force: force)
if !isHTTPURL && fm.fileExists(atPath: localTarPath) {
try await ClientKernel.installKernelFromTar(
tarFile: localTarPath,
kernelFilePath: binaryPath,
platform: platform,
expectedDigest: digest,
force: force)
return
}
guard let remoteURL = URL(string: tarPath) else {
guard let remoteURL else {
throw ContainerizationError(.invalidArgument, message: "invalid remote URL '\(tarPath)' for argument '--tar'. Missing protocol?")
}
try await Self.downloadAndInstallWithProgressBar(tarRemoteURL: remoteURL, kernelFilePath: binaryPath, platform: platform, force: force)
guard let digest else {
throw ArgumentParser.ValidationError("'--digest' is required when '--tar' is a remote URL")
}
try await Self.downloadAndInstallWithProgressBar(
tarRemoteURL: remoteURL,
kernelFilePath: binaryPath,
platform: platform,
expectedDigest: digest,
force: force)
}
private func getSystemPlatform() throws -> SystemPlatform {
@@ -107,10 +133,16 @@ extension Application {
}
}
static func downloadAndInstallWithProgressBar(tarRemoteURL: URL, kernelFilePath: String, platform: SystemPlatform = .current, force: Bool) async throws {
static func downloadAndInstallWithProgressBar(
tarRemoteURL: URL,
kernelFilePath: String,
platform: SystemPlatform = .current,
expectedDigest: String,
force: Bool
) async throws {
let progressConfig = try ProgressConfig(
showTasks: true,
totalTasks: 2
totalTasks: 3
)
let progress = ProgressBar(config: progressConfig)
defer {
@@ -118,7 +150,12 @@ extension Application {
}
progress.start()
try await ClientKernel.installKernelFromTar(
tarFile: tarRemoteURL.absoluteString, kernelFilePath: kernelFilePath, platform: platform, progressUpdate: progress.handler, force: force)
tarFile: tarRemoteURL.absoluteString,
kernelFilePath: kernelFilePath,
platform: platform,
progressUpdate: progress.handler,
expectedDigest: expectedDigest,
force: force)
progress.finish()
}
@@ -157,7 +157,10 @@ extension Application {
guard await !kernelExists() else {
return
}
try await installDefaultKernel(kernelURL: containerSystemConfig.kernel.url, kernelBinaryPath: containerSystemConfig.kernel.binaryPath)
try await installDefaultKernel(
kernelURL: containerSystemConfig.kernel.url,
kernelBinaryPath: containerSystemConfig.kernel.binaryPath,
kernelDigest: containerSystemConfig.kernel.digest)
}
private func installInitialFilesystem(initImage: String) async throws {
@@ -171,7 +174,7 @@ extension Application {
}
}
private func installDefaultKernel(kernelURL: URL, kernelBinaryPath: String) async throws {
private func installDefaultKernel(kernelURL: URL, kernelBinaryPath: String, kernelDigest: String) async throws {
var shouldInstallKernel = false
if kernelInstall == nil {
print("No default kernel configured.")
@@ -191,7 +194,11 @@ extension Application {
return
}
log.info("Installing kernel...")
try await KernelSet.downloadAndInstallWithProgressBar(tarRemoteURL: kernelURL, kernelFilePath: kernelBinaryPath, force: true)
try await KernelSet.downloadAndInstallWithProgressBar(
tarRemoteURL: kernelURL,
kernelFilePath: kernelBinaryPath,
expectedDigest: kernelDigest,
force: true)
}
private func initImageExists(containerSystemConfig: ContainerSystemConfig) async -> Bool {
@@ -168,21 +168,22 @@ final public class KernelConfig: Codable, Sendable {
public static let defaultBinaryPath = "opt/kata/share/kata-containers/vmlinux-6.18.15-186"
public static let defaultURL: URL =
URL(string: "https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst")!
public static let defaultDigest = "sha256:f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"
private enum CodingKeys: String, CodingKey {
case binaryPath
case url
case digest
}
public let binaryPath: String
public let url: URL
public let digest: String
public init(
binaryPath: String = defaultBinaryPath,
url: URL = defaultURL
) {
public init(binaryPath: String = defaultBinaryPath, url: URL = defaultURL, digest: String = defaultDigest) {
self.binaryPath = binaryPath
self.url = url
self.digest = digest
}
public init(from decoder: any Decoder) throws {
@@ -190,13 +191,27 @@ final public class KernelConfig: Codable, Sendable {
self.binaryPath =
try container.decodeIfPresent(String.self, forKey: .binaryPath)
?? Self.defaultBinaryPath
if let urlString = try container.decodeIfPresent(String.self, forKey: .url),
let parsed = URL(string: urlString)
{
if let urlString = try container.decodeIfPresent(String.self, forKey: .url) {
guard let parsed = URL(string: urlString) else {
throw DecodingError.dataCorruptedError(
forKey: .url,
in: container,
debugDescription: "invalid kernel URL '\(urlString)'")
}
self.url = parsed
} else {
self.url = Self.defaultURL
}
if let digest = try container.decodeIfPresent(String.self, forKey: .digest) {
self.digest = digest
} else if self.url.absoluteString == Self.defaultURL.absoluteString {
self.digest = Self.defaultDigest
} else {
throw DecodingError.dataCorruptedError(
forKey: .digest,
in: container,
debugDescription: "kernel.digest is required when kernel.url is not the default URL")
}
}
// JSONEncoder special-cases URL to encode as absoluteString, but third-party
@@ -209,6 +224,7 @@ final public class KernelConfig: Codable, Sendable {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(binaryPath, forKey: .binaryPath)
try container.encode(url.absoluteString, forKey: .url)
try container.encode(digest, forKey: .digest)
}
}
@@ -42,15 +42,23 @@ extension ClientKernel {
try await client.send(message)
}
public static func installKernelFromTar(tarFile: String, kernelFilePath: String, platform: SystemPlatform, progressUpdate: ProgressUpdateHandler? = nil, force: Bool)
async throws
{
public static func installKernelFromTar(
tarFile: String,
kernelFilePath: String,
platform: SystemPlatform,
progressUpdate: ProgressUpdateHandler? = nil,
expectedDigest: String? = nil,
force: Bool
) async throws {
let client = newClient()
let message = XPCMessage(route: .installKernel)
message.set(key: .kernelTarURL, value: tarFile)
message.set(key: .kernelFilePath, value: kernelFilePath)
message.set(key: .kernelForce, value: force)
if let expectedDigest {
message.set(key: .kernelDigest, value: expectedDigest)
}
let platformData = try JSONEncoder().encode(platform)
message.set(key: .systemPlatform, value: platformData)
@@ -112,6 +112,7 @@ public enum XPCKeys: String {
case kernelFilePath
case systemPlatform
case kernelForce
case kernelDigest
/// Init image reference
case initImage
@@ -35,6 +35,7 @@ public struct KernelHarness: Sendable {
let kernelFilePath = try message.kernelFilePath()
let platform = try message.platform()
let force = try message.kernelForce()
let expectedDigest = message.kernelDigest()
guard let kernelTarUrl = try message.kernelTarURL() else {
// We have been given a path to a kernel binary on disk
@@ -47,7 +48,12 @@ public struct KernelHarness: Sendable {
let progressUpdateService = ProgressUpdateService(message: message)
try await self.service.installKernelFrom(
tar: kernelTarUrl, kernelFilePath: kernelFilePath, platform: platform, progressUpdate: progressUpdateService?.handler, force: force)
tar: kernelTarUrl,
kernelFilePath: kernelFilePath,
platform: platform,
progressUpdate: progressUpdateService?.handler,
expectedDigest: expectedDigest,
force: force)
return message.reply()
}
@@ -85,13 +91,17 @@ extension XPCMessage {
guard let kernelTarURLString = self.string(key: .kernelTarURL) else {
return nil
}
guard let k = URL(string: kernelTarURLString) else {
throw ContainerizationError(.invalidArgument, message: "cannot parse URL from \(kernelTarURLString)")
if let k = URL(string: kernelTarURLString), k.scheme != nil {
return k
}
return k
return URL(fileURLWithPath: kernelTarURLString)
}
fileprivate func kernelForce() throws -> Bool {
self.bool(key: .kernelForce)
}
fileprivate func kernelDigest() -> String? {
self.string(key: .kernelDigest)
}
}
@@ -19,6 +19,7 @@ import Containerization
import ContainerizationArchive
import ContainerizationError
import ContainerizationExtras
import CryptoKit
import Foundation
import Logging
import TerminalProgress
@@ -29,6 +30,11 @@ public actor KernelService {
private let log: Logger
private let kernelDirectory: URL
private struct ExpectedDigest {
let algorithm: String
let hex: String
}
public init(log: Logger, appRoot: URL) throws {
self.log = log
self.kernelDirectory = appRoot.appending(path: "kernels")
@@ -81,7 +87,14 @@ public actor KernelService {
/// Copies a kernel binary from inside of tar file into the managed kernels directory
/// as the default kernel for the provided platform.
/// The parameter `tar` maybe a location to a local file on disk, or a remote URL.
public func installKernelFrom(tar: URL, kernelFilePath: String, platform: SystemPlatform, progressUpdate: ProgressUpdateHandler?, force: Bool) async throws {
public func installKernelFrom(
tar: URL,
kernelFilePath: String,
platform: SystemPlatform,
progressUpdate: ProgressUpdateHandler?,
expectedDigest: String? = nil,
force: Bool
) async throws {
log.debug(
"KernelService: enter",
metadata: [
@@ -103,39 +116,110 @@ public actor KernelService {
)
}
var tarFile = tar
let localTarPath = tar.scheme == nil || tar.isFileURL ? tar.path : nil
let isLocalTar = localTarPath.map { FileManager.default.fileExists(atPath: $0) } ?? false
if isLocalTar, let localTarPath {
tarFile = URL(fileURLWithPath: localTarPath)
}
guard isLocalTar || expectedDigest != nil else {
throw ContainerizationError(
.invalidArgument,
message: "kernel archive digest is required for remote URL '\(tar)'"
)
}
let expectedDigest = try expectedDigest.map(Self.parseExpectedDigest)
let tempDir = FileManager.default.uniqueTemporaryDirectory()
defer {
try? FileManager.default.removeItem(at: tempDir)
}
await progressUpdate?([
.setDescription("Downloading kernel")
.setDescription(isLocalTar ? "Reading kernel archive" : "Downloading kernel")
])
let taskManager = ProgressTaskCoordinator()
let downloadTask = await taskManager.startTask()
var tarFile = tar
if !FileManager.default.fileExists(atPath: tar.absoluteString) {
if !isLocalTar {
let taskManager = ProgressTaskCoordinator()
let downloadTask = await taskManager.startTask()
self.log.debug("KernelService: start download", metadata: ["tar": "\(tar)"])
tarFile = tempDir.appendingPathComponent(tar.lastPathComponent)
var downloadProgressUpdate: ProgressUpdateHandler?
if let progressUpdate {
downloadProgressUpdate = ProgressTaskCoordinator.handler(for: downloadTask, from: progressUpdate)
}
try await ContainerAPIClient.FileDownloader.downloadFile(url: tar, to: tarFile, progressUpdate: downloadProgressUpdate)
try await ContainerAPIClient.FileDownloader.downloadFile(
url: tar,
to: tarFile,
progressUpdate: downloadProgressUpdate)
await taskManager.finish()
}
await progressUpdate?([
.addTasks(1)
])
if let expectedDigest {
await progressUpdate?([
.setDescription("Verifying kernel archive")
])
try Self.verifyDigest(of: tarFile, expected: expectedDigest)
await progressUpdate?([
.addTasks(1)
])
}
await taskManager.finish()
await progressUpdate?([
.setDescription("Unpacking kernel")
])
let kernelFile = try self.extractFile(tarFile: tarFile, at: kernelFilePath, to: tempDir)
try self.installKernel(kernelFile: kernelFile, platform: platform, force: force)
await progressUpdate?([
.addTasks(1)
])
if !FileManager.default.fileExists(atPath: tar.absoluteString) {
if !isLocalTar {
try FileManager.default.removeItem(at: tarFile)
}
}
private static func verifyDigest(of file: URL, expected: ExpectedDigest) throws {
let actualDigest = try sha256Hex(of: file)
try verifyDigest(actualSHA256Hex: actualDigest, expected: expected)
}
private static func verifyDigest(actualSHA256Hex actualDigest: String, expected: ExpectedDigest) throws {
guard actualDigest == expected.hex else {
throw ContainerizationError(
.invalidState,
message: "kernel archive digest mismatch: expected sha256:\(expected.hex), got sha256:\(actualDigest)"
)
}
}
private static func parseExpectedDigest(_ expected: String) throws -> ExpectedDigest {
let parts = expected.lowercased().split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
guard parts.count == 2, !parts[0].isEmpty, !parts[1].isEmpty else {
throw ContainerizationError(.invalidArgument, message: "invalid digest value '\(expected)': expected '<algorithm>:<hex>'")
}
let digest = ExpectedDigest(algorithm: String(parts[0]), hex: String(parts[1]))
guard digest.algorithm == "sha256" else {
throw ContainerizationError(.unsupported, message: "unsupported digest algorithm '\(digest.algorithm)'")
}
guard digest.hex.count == 64, digest.hex.utf8.allSatisfy({ ($0 >= 48 && $0 <= 57) || ($0 >= 97 && $0 <= 102) }) else {
throw ContainerizationError(.invalidArgument, message: "invalid sha256 digest value '\(expected)'")
}
return digest
}
static func sha256Hex(of file: URL) throws -> String {
var hasher = SHA256()
let handle = try FileHandle(forReadingFrom: file)
defer { try? handle.close() }
while let data = try handle.read(upToCount: Int(1.mib())), !data.isEmpty {
hasher.update(data: data)
}
return hasher.finalize().map { String(format: "%02x", $0) }.joined()
}
private func setDefaultKernel(name: String, platform: SystemPlatform) throws {
log.debug(
"KernelService: enter",
@@ -0,0 +1,162 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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 Containerization
import ContainerizationArchive
import ContainerizationError
import CryptoKit
import Foundation
import Logging
import Testing
@testable import ContainerAPIService
struct KernelServiceTests {
@Test func installKernelFromLocalTarVerifiesDigest() async throws {
try await withTempDir { tempDir in
let kernelPath = "boot/vmlinux"
let kernelData = Data("kernel binary".utf8)
let tarFile = try Self.writeTar(
at: tempDir.appendingPathComponent("kernel.tar"),
path: kernelPath,
data: kernelData)
let service = try KernelService(
log: Logger(label: "com.apple.container.test.kernel-service"),
appRoot: tempDir.appendingPathComponent("app"))
let digest = try KernelService.sha256Hex(of: tarFile)
try await service.installKernelFrom(
tar: URL(string: tarFile.path)!,
kernelFilePath: kernelPath,
platform: .linuxArm,
progressUpdate: nil,
expectedDigest: "sha256:\(digest)",
force: false)
let kernel = try await service.getDefaultKernel(platform: .linuxArm)
#expect(try Data(contentsOf: kernel.path) == kernelData)
}
}
@Test func installKernelFromLocalTarRejectsDigestMismatchWithoutInstalling() async throws {
try await withTempDir { tempDir in
let kernelPath = "boot/vmlinux"
let kernelData = Data("kernel binary".utf8)
let tarFile = try Self.writeTar(
at: tempDir.appendingPathComponent("kernel.tar"),
path: kernelPath,
data: kernelData)
let service = try KernelService(
log: Logger(label: "com.apple.container.test.kernel-service"),
appRoot: tempDir.appendingPathComponent("app"))
let wrongDigest = String(repeating: "0", count: 64)
await #expect(throws: ContainerizationError.self) {
try await service.installKernelFrom(
tar: URL(fileURLWithPath: tarFile.path),
kernelFilePath: kernelPath,
platform: .linuxArm,
progressUpdate: nil,
expectedDigest: "sha256:\(wrongDigest)",
force: false)
}
await #expect(throws: ContainerizationError.self) {
_ = try await service.getDefaultKernel(platform: .linuxArm)
}
}
}
@Test func installKernelFromLocalTarRejectsInvalidDigestValues() async throws {
try await withTempDir { tempDir in
let kernelPath = "boot/vmlinux"
let kernelData = Data("kernel binary".utf8)
let tarFile = try Self.writeTar(
at: tempDir.appendingPathComponent("kernel.tar"),
path: kernelPath,
data: kernelData)
let service = try KernelService(
log: Logger(label: "com.apple.container.test.kernel-service"),
appRoot: tempDir.appendingPathComponent("app"))
let sha256 = try KernelService.sha256Hex(of: tarFile)
let sha1 = try Self.sha1Hex(of: tarFile)
let invalidDigests = [
"sha256-not-a-digest",
"sha1:\(sha1)",
"sha256:not-a-digest",
String(repeating: "0", count: 64),
"sha256:\(String(sha256.dropLast(2)))",
"sha256:\(sha1)",
]
for digest in invalidDigests {
await #expect(throws: ContainerizationError.self) {
try await service.installKernelFrom(
tar: URL(fileURLWithPath: tarFile.path),
kernelFilePath: kernelPath,
platform: .linuxArm,
progressUpdate: nil,
expectedDigest: digest,
force: false)
}
}
await #expect(throws: ContainerizationError.self) {
_ = try await service.getDefaultKernel(platform: .linuxArm)
}
}
}
@Test func installKernelFromRemoteTarRequiresDigest() async throws {
try await withTempDir { tempDir in
let service = try KernelService(
log: Logger(label: "com.apple.container.test.kernel-service"),
appRoot: tempDir.appendingPathComponent("app"))
await #expect(throws: ContainerizationError.self) {
try await service.installKernelFrom(
tar: URL(string: "https://example.com/kernel.tar")!,
kernelFilePath: "boot/vmlinux",
platform: .linuxArm,
progressUpdate: nil,
expectedDigest: nil,
force: false)
}
}
}
private static func writeTar(at tarFile: URL, path: String, data: Data) throws -> URL {
let archiver = try ArchiveWriter(format: .paxRestricted, filter: .none, file: tarFile)
let entry = WriteEntry()
entry.path = path
entry.fileType = .regular
entry.permissions = 0o644
entry.size = numericCast(data.count)
try archiver.writeEntry(entry: entry, data: data)
try archiver.finishEncoding()
return tarFile
}
private static func sha1Hex(of file: URL) throws -> String {
let data = try Data(contentsOf: file)
return Insecure.SHA1.hash(data: data).map { String(format: "%02x", $0) }.joined()
}
private func withTempDir(body: (URL) async throws -> Void) async throws {
let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: dir) }
try await body(dir)
}
}
@@ -95,6 +95,7 @@ struct ConfigurationLoaderTests {
#expect(!config.vminit.image.isEmpty)
#expect(!config.kernel.binaryPath.isEmpty)
#expect(!config.kernel.url.absoluteString.isEmpty)
#expect(config.kernel.digest == KernelConfig.defaultDigest)
#expect(config.network.subnet == nil)
#expect(config.network.subnetv6 == nil)
#expect(config.registry.domain == "docker.io")
@@ -120,6 +121,7 @@ struct ConfigurationLoaderTests {
[kernel]
binaryPath = "custom/path"
url = "https://example.com/kernel.tar"
digest = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
[network]
subnet = "10.0.0.1/16"
@@ -147,6 +149,7 @@ struct ConfigurationLoaderTests {
#expect(config.vminit.image == "custom-init:latest")
#expect(config.kernel.binaryPath == "custom/path")
#expect(config.kernel.url.absoluteString == "https://example.com/kernel.tar")
#expect(config.kernel.digest == "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
let expectedSubnet = try CIDRv4("10.0.0.1/16")
let expectedSubnetV6 = try CIDRv6("fd01::/48")
#expect(config.network.subnet == expectedSubnet)
@@ -173,6 +176,51 @@ struct ConfigurationLoaderTests {
}
}
@Test func customKernelURLWithoutDigestThrows() async throws {
try await TemporaryStorage.withTempDir { tempDir in
let toml = """
[kernel]
url = "https://example.com/custom-kernel.tar"
"""
let tmpFile = tempDir.appending("test.toml")
try Self.writeToml(toml, to: tmpFile)
await #expect(throws: (any Error).self) {
let _: ContainerSystemConfig = try await ConfigurationLoader.load(configurationFiles: [tmpFile])
}
}
}
@Test func layeredCustomKernelURLCanUseDigestFromLowerLayer() async throws {
try await TemporaryStorage.withTempDir { tempDir in
let userFile = tempDir.appending("user.toml")
let systemFile = tempDir.appending("system.toml")
try Self.writeToml(
"""
[kernel]
url = "https://example.com/custom-kernel.tar"
""", to: userFile)
try Self.writeToml(
"""
[kernel]
digest = "\(KernelConfig.defaultDigest)"
""", to: systemFile)
let config: ContainerSystemConfig = try await ConfigurationLoader.load(
configurationFiles: [userFile, systemFile])
#expect(config.kernel.url.absoluteString == "https://example.com/custom-kernel.tar")
#expect(config.kernel.digest == KernelConfig.defaultDigest)
}
}
@Test func customKernelURLWithDigestCanBeConstructed() {
let digest = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
let config = KernelConfig(url: URL(string: "https://example.com/custom-kernel.tar")!, digest: digest)
#expect(config.url.absoluteString == "https://example.com/custom-kernel.tar")
#expect(config.digest == digest)
}
@Test func unknownKeysIgnored() async throws {
try await TemporaryStorage.withTempDir { tempDir in
let toml = """
@@ -26,6 +26,7 @@ import Testing
struct TestCLIKernelSetSerial {
private let remoteTar = ContainerSystemConfig().kernel.url
private let defaultBinaryPath = ContainerSystemConfig().kernel.binaryPath
private let defaultDigest = KernelConfig.defaultDigest
/// Kernel release string parsed from the binary filename.
///
@@ -49,6 +50,30 @@ struct TestCLIKernelSetSerial {
// MARK: - Tests
@Test func remoteTarCannotBeShadowedByLocalPath() async throws {
try await ContainerFixture.with { f in
let shadow = URL(filePath: f.testDir.string)
.appending(path: "https:")
.appending(path: "example.com")
.appending(path: "kernel.tar")
try FileManager.default.createDirectory(
at: shadow.deletingLastPathComponent(),
withIntermediateDirectories: true)
try Data().write(to: shadow)
let result = try f.run(
[
"system", "kernel", "set",
"--tar", "https://example.com/kernel.tar",
"--binary", "vmlinux",
],
currentDirectory: f.testDir)
#expect(result.status != 0)
#expect(result.error.contains("'--digest' is required when '--tar' is a remote URL"))
}
}
@Test func fromLocalTar() async throws {
let symlinkBinaryPath = URL(filePath: defaultBinaryPath)
.deletingLastPathComponent()
@@ -60,7 +85,13 @@ struct TestCLIKernelSetSerial {
let tempDir = URL(filePath: f.testDir.string)
let localTarPath = tempDir.appending(path: remoteTar.lastPathComponent)
try await ContainerAPIClient.FileDownloader.downloadFile(url: remoteTar, to: localTarPath)
try f.run(["system", "kernel", "set", "--force", "--tar", localTarPath.path, "--binary", symlinkBinaryPath]).check()
try f.run([
"system", "kernel", "set",
"--force",
"--tar", localTarPath.path,
"--binary", symlinkBinaryPath,
"--digest", defaultDigest,
]).check()
try await validateGuestKernel(f)
}
}
@@ -73,7 +104,13 @@ struct TestCLIKernelSetSerial {
try await ContainerFixture.with { f in
f.addCleanup { resetKernelToRecommended(f) }
try f.run(["system", "kernel", "set", "--force", "--tar", remoteTar.absoluteString, "--binary", symlinkBinaryPath]).check()
try f.run([
"system", "kernel", "set",
"--force",
"--tar", remoteTar.absoluteString,
"--binary", symlinkBinaryPath,
"--digest", defaultDigest,
]).check()
try await validateGuestKernel(f)
}
}
+2 -1
View File
@@ -1543,7 +1543,7 @@ Installs or updates the Linux kernel used by the container runtime on macOS host
**Usage**
```bash
container system kernel set [--arch <arch>] [--binary <binary>] [--force] [--recommended] [--tar <tar>] [--debug]
container system kernel set [--arch <arch>] [--binary <binary>] [--force] [--recommended] [--tar <tar>] [--digest <digest>] [--debug]
```
**Options**
@@ -1553,6 +1553,7 @@ container system kernel set [--arch <arch>] [--binary <binary>] [--force] [--rec
* `--force`: Overwrites an existing kernel with the same name
* `--recommended`: Download and install the recommended kernel as the default (takes precedence over all other flags)
* `--tar <tar>`: Filesystem path or remote URL to a tar archive containing a kernel file
* `--digest <digest>`: Expected digest for the tar archive, for example `sha256:<hex>`. Required when `--tar` is a remote URL.
### `container system property list (ls)`
+6 -5
View File
@@ -15,7 +15,7 @@ Source of truth: [`Sources/ContainerPersistence/ContainerSystemConfig.swift`](..
[build] # builder VM resources and image
[container] # default per-container resources
[dns] # default DNS domain for DNS resolution on host
[kernel] # guest kernel binary path and download URL
[kernel] # guest kernel binary path, download URL, and digest
[network] # default subnets for new networks
[registry] # default registry domain
[vminit] # default vminitd image to use
@@ -54,10 +54,11 @@ Defaults applied when `container run` / `container create` is invoked without `-
Guest kernel used when launching container VMs. Defaults change per release as kernels are bumped — check the [source](../Sources/ContainerPersistence/ContainerSystemConfig.swift) for current values.
| Key | Type | Default | Description |
|--------------|----------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| `binaryPath` | `String` | `"opt/kata/share/kata-containers/vmlinux-6.18.15-186"` | Path **inside** the downloaded kernel archive that points to the kernel binary. |
| `url` | `URL` | `"https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst"` | Archive to download when no kernel is installed. Encoded and decoded as a plain string in TOML. |
| Key | Type | Default | Description |
|--------------|-----------|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| `binaryPath` | `String` | `"opt/kata/share/kata-containers/vmlinux-6.18.15-186"` | Path **inside** the downloaded kernel archive that points to the kernel binary. |
| `url` | `URL` | `"https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst"` | Archive to download when no kernel is installed. Encoded and decoded as a plain string in TOML. |
| `digest` | `String` | `"sha256:f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"` | Expected digest for the archive, for example `sha256:<hex>`. Required when configuring a custom `url`. |
## `[network]`
+3 -2
View File
@@ -659,8 +659,9 @@ memory = "1gb"
domain = "test"
[kernel]
binaryPath = "opt/kata/share/kata-containers/vmlinux-6.18.5-177"
url = "https://github.com/kata-containers/kata-containers/releases/download/3.26.0/kata-static-3.26.0-arm64.tar.zst"
binaryPath = "opt/kata/share/kata-containers/vmlinux-6.18.15-186"
url = "https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst"
digest = "sha256:f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"
[network]
@@ -77,6 +77,7 @@ domain = "test"
[kernel]
binaryPath = "opt/kata/share/kata-containers/vmlinux-6.18.15-186"
url = "https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst"
digest = "sha256:f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"
[network]