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
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ let package = Package(
name: "ContainerAPIServiceTests",
dependencies: [
.product(name: "Containerization", package: "containerization"),
"ContainerAPIService",
"ContainerResource",
"ContainerRuntimeLinuxClient",
"ContainerRuntimeClient",
Expand Down
41 changes: 36 additions & 5 deletions Sources/ContainerCommands/System/Kernel/KernelSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ 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 SHA-256 digest for the tar archive, as hex or with a sha256: prefix")
var sha256: String? = nil

@OptionGroup
public var logOptions: Flags.Logging

Expand All @@ -58,7 +61,11 @@ extension Application {
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,
expectedSha256: containerSystemConfig.kernel.sha256,
force: force)
return
}
guard tarPath != nil else {
Expand All @@ -68,6 +75,9 @@ extension Application {
}

private func setKernelFromBinary() async throws {
guard sha256 == nil else {
throw ArgumentParser.ValidationError("'--sha256' can only be used with '--tar'")
}
guard let binaryPath else {
throw ArgumentParser.ValidationError("missing argument '--binary'")
}
Expand All @@ -87,13 +97,23 @@ extension Application {
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)
try await ClientKernel.installKernelFromTar(
tarFile: localTarPath,
kernelFilePath: binaryPath,
platform: platform,
expectedSha256: sha256,
force: force)
return
}
guard let remoteURL = URL(string: tarPath) 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)
try await Self.downloadAndInstallWithProgressBar(
tarRemoteURL: remoteURL,
kernelFilePath: binaryPath,
platform: platform,
expectedSha256: sha256,
force: force)
}

private func getSystemPlatform() throws -> SystemPlatform {
Expand All @@ -107,7 +127,13 @@ 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,
expectedSha256: String? = nil,
force: Bool
) async throws {
let progressConfig = try ProgressConfig(
showTasks: true,
totalTasks: 2
Expand All @@ -118,7 +144,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,
expectedSha256: expectedSha256,
force: force)
progress.finish()
}

Expand Down
13 changes: 10 additions & 3 deletions Sources/ContainerCommands/System/SystemStart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
kernelSha256: containerSystemConfig.kernel.sha256)
}

private func installInitialFilesystem(initImage: String) async throws {
Expand All @@ -171,7 +174,7 @@ extension Application {
}
}

private func installDefaultKernel(kernelURL: URL, kernelBinaryPath: String) async throws {
private func installDefaultKernel(kernelURL: URL, kernelBinaryPath: String, kernelSha256: String?) async throws {
var shouldInstallKernel = false
if kernelInstall == nil {
print("No default kernel configured.")
Expand All @@ -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,
expectedSha256: kernelSha256,
force: true)
}

private func initImageExists(containerSystemConfig: ContainerSystemConfig) async -> Bool {
Expand Down
11 changes: 10 additions & 1 deletion Sources/ContainerPersistence/ContainerSystemConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,21 +168,26 @@ 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 defaultSha256 = "f63d54507d1f18635d94475077e4c2330de4d8e05cedf25f7c38f063b0e66a91"

private enum CodingKeys: String, CodingKey {
case binaryPath
case url
case sha256
}

public let binaryPath: String
public let url: URL
public let sha256: String?

public init(
binaryPath: String = defaultBinaryPath,
url: URL = defaultURL
url: URL = defaultURL,
sha256: String? = nil
) {
self.binaryPath = binaryPath
self.url = url
self.sha256 = sha256 ?? (url.absoluteString == Self.defaultURL.absoluteString ? Self.defaultSha256 : nil)
}

public init(from decoder: any Decoder) throws {
Expand All @@ -197,6 +202,9 @@ final public class KernelConfig: Codable, Sendable {
} else {
self.url = Self.defaultURL
}
self.sha256 =
try container.decodeIfPresent(String.self, forKey: .sha256)
?? (self.url.absoluteString == Self.defaultURL.absoluteString ? Self.defaultSha256 : nil)
}

// JSONEncoder special-cases URL to encode as absoluteString, but third-party
Expand All @@ -209,6 +217,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.encodeIfPresent(sha256, forKey: .sha256)
}
}

Expand Down
14 changes: 11 additions & 3 deletions Sources/Services/ContainerAPIService/Client/ClientKernel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
expectedSha256: 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 expectedSha256 {
message.set(key: .kernelSha256, value: expectedSha256)
}

let platformData = try JSONEncoder().encode(platform)
message.set(key: .systemPlatform, value: platformData)
Expand Down
1 change: 1 addition & 0 deletions Sources/Services/ContainerAPIService/Client/XPC+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public enum XPCKeys: String {
case kernelFilePath
case systemPlatform
case kernelForce
case kernelSha256

/// Init image reference
case initImage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public struct KernelHarness: Sendable {
let kernelFilePath = try message.kernelFilePath()
let platform = try message.platform()
let force = try message.kernelForce()
let expectedSha256 = message.kernelSha256()

guard let kernelTarUrl = try message.kernelTarURL() else {
// We have been given a path to a kernel binary on disk
Expand All @@ -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,
expectedSha256: expectedSha256,
force: force)
return message.reply()
}

Expand Down Expand Up @@ -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 kernelSha256() -> String? {
self.string(key: .kernelSha256)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Containerization
import ContainerizationArchive
import ContainerizationError
import ContainerizationExtras
import CryptoKit
import Foundation
import Logging
import TerminalProgress
Expand Down Expand Up @@ -81,7 +82,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?,
expectedSha256: String? = nil,
force: Bool
) async throws {
log.debug(
"KernelService: enter",
metadata: [
Expand Down Expand Up @@ -114,7 +122,12 @@ public actor KernelService {
let taskManager = ProgressTaskCoordinator()
let downloadTask = await taskManager.startTask()
var tarFile = tar
if !FileManager.default.fileExists(atPath: tar.absoluteString) {
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)
}
if !isLocalTar {
self.log.debug("KernelService: start download", metadata: ["tar": "\(tar)"])
tarFile = tempDir.appendingPathComponent(tar.lastPathComponent)
var downloadProgressUpdate: ProgressUpdateHandler?
Expand All @@ -125,17 +138,50 @@ public actor KernelService {
}
await taskManager.finish()

if let expectedSha256 {
await progressUpdate?([
.setDescription("Verifying kernel download")
])
try Self.verifySHA256Digest(of: tarFile, expected: expectedSha256)
}

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)

if !FileManager.default.fileExists(atPath: tar.absoluteString) {
if !isLocalTar {
try FileManager.default.removeItem(at: tarFile)
}
}

static func verifySHA256Digest(of file: URL, expected: String) throws {
let lowercased = expected.lowercased()
let expectedDigest = lowercased.hasPrefix("sha256:") ? String(lowercased.dropFirst("sha256:".count)) : lowercased
guard expectedDigest.count == 64, expectedDigest.utf8.allSatisfy({ ($0 >= 48 && $0 <= 57) || ($0 >= 97 && $0 <= 102) }) else {
throw ContainerizationError(.invalidArgument, message: "invalid SHA-256 digest '\(expected)'")
}

let actualDigest = try sha256Hex(of: file)
guard actualDigest == expectedDigest else {
throw ContainerizationError(
.invalidState,
message: "kernel archive digest mismatch: expected sha256:\(expectedDigest), got sha256:\(actualDigest)"
)
}
}

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",
Expand Down
5 changes: 5 additions & 0 deletions Tests/CLITests/Subcommands/System/TestKernelSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import Testing
class TestCLIKernelSet: CLITest {
let remoteTar = ContainerSystemConfig().kernel.url
let defaultBinaryPath = ContainerSystemConfig().kernel.binaryPath
let defaultSha256 = KernelConfig.defaultSha256

deinit {
try? resetDefaultBinary()
Expand Down Expand Up @@ -85,6 +86,8 @@ class TestCLIKernelSet: CLITest {
localTarPath.path,
"--binary",
symlinkBinaryPath,
"--sha256",
defaultSha256,
]

try doKernelSet(extraArgs: extraArgs)
Expand All @@ -100,6 +103,8 @@ class TestCLIKernelSet: CLITest {
remoteTar.absoluteString,
"--binary",
symlinkBinaryPath,
"--sha256",
defaultSha256,
]

try doKernelSet(extraArgs: extraArgs)
Expand Down
Loading