镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
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
15 changes: 13 additions & 2 deletions Sources/Containerization/Vminitd.swift
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,19 @@ extension Vminitd: VirtualMachineAgent {
$0.containerID = containerID
}
}
let resp = try await client.startProcess(request)
return resp.pid
do {
let resp = try await client.startProcess(request)
return resp.pid
} catch let error as RPCError {
switch error.code {
case .notFound:
throw ContainerizationError(.notFound, message: error.message, cause: error)
case .permissionDenied:
throw ContainerizationError(.notExecutable, message: error.message, cause: error)
default:
throw error
}
}
}

public func signalProcess(id: String, containerID: String?, signal: Int32) async throws {
Expand Down
5 changes: 5 additions & 0 deletions Sources/ContainerizationError/ContainerizationError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ extension ContainerizationError {
case timeout
case unsupported
case interrupted
case notExecutable
}

private var value: Value
Expand Down Expand Up @@ -166,6 +167,10 @@ extension ContainerizationError {
public static var interrupted: Self {
Self(.interrupted)
}

public static var notExecutable: Self {
Self(.notExecutable)
}
}
}

Expand Down
31 changes: 30 additions & 1 deletion Sources/Integration/ContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,36 @@ extension IntegrationSuite {
} catch {
try? await container.stop()
guard let err = error as? ContainerizationError,
err.isCode(.internalError), err.description.contains("failed to find target executable")
err.isCode(.notFound), err.description.contains("failed to find target executable")
else {
throw error
}
}
}

func testProcessNotExecutable() async throws {
let id = "test-process-not-executable"
let bs = try await bootstrap(id)

let buffer = BufferWriter()
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.arguments = ["/etc/passwd"]
config.process.stdout = buffer
config.bootLog = bs.bootLog
}

do {
try await container.create()
try await container.start()

let _ = try await container.wait()
try await container.stop()

throw IntegrationError.assert(msg: "process didn't throw 'not executable' error")
} catch {
try? await container.stop()
guard let err = error as? ContainerizationError,
err.isCode(.notExecutable), err.description.contains("failed to exec")
else {
throw error
}
Expand Down
1 change: 1 addition & 0 deletions Sources/Integration/Suite.swift
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ struct IntegrationSuite: AsyncParsableCommand {
Test("process false", testProcessFalse),
Test("process echo hi", testProcessEchoHi),
Test("process no executable", testProcessNoExecutable),
Test("process not executable", testProcessNotExecutable),
Test("process user", testProcessUser),
Test("process stdin", testProcessStdin),
Test("process home envvar", testProcessHomeEnvvar),
Expand Down
51 changes: 51 additions & 0 deletions Tests/VminitdCoreTests/ManagedProcessErrorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//===----------------------------------------------------------------------===//
// Copyright © 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.
//===----------------------------------------------------------------------===//

#if os(Linux)

import ContainerizationError
import Testing

@testable import VminitdCore

@Suite("ManagedProcess vmexec error")
struct ManagedProcessErrorTests {
@Test func missingExecutableKeepsNotFound() {
let error = ManagedProcess.vmexecError(
ContainerizationError(.notFound, message: "failed to find target executable foo").description
)
#expect(error.code == .notFound)
#expect(error.message.contains("failed to find target executable foo"))
}

@Test func notExecutableKeepsCode() {
let error = ManagedProcess.vmexecError(
ContainerizationError(.notExecutable, message: "failed to exec [/etc/passwd]").description + "\n"
)
#expect(error.code == .notExecutable)
}

@Test(arguments: [
ContainerizationError(.internalError, message: "failed to change directory").description,
ContainerizationError(.invalidArgument, message: "bad").description,
"unstructured failure",
])
func otherErrorsAreInternal(errorString: String) {
#expect(ManagedProcess.vmexecError(errorString).code == .internalError)
}
}

#endif
20 changes: 11 additions & 9 deletions vminitd/Sources/VminitdCore/ManagedProcess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,7 @@ extension ManagedProcess {
let errorString = String(data: errorData, encoding: .utf8),
!errorString.isEmpty
{
throw ContainerizationError(
.internalError,
message: "vmexec error: \(errorString.trimmingCharacters(in: .whitespacesAndNewlines))"
)
throw Self.vmexecError(errorString)
}

log.info(
Expand All @@ -259,16 +256,21 @@ extension ManagedProcess {
let errorString = String(data: errorData, encoding: .utf8),
!errorString.isEmpty
{
throw ContainerizationError(
.internalError,
message: "vmexec error: \(errorString.trimmingCharacters(in: .whitespacesAndNewlines))",
cause: error
)
throw Self.vmexecError(errorString, cause: error)
}
throw error
}
}

/// vmexec writes `ContainerizationError.description` to the error pipe, so
/// the text starts with the error code.
static func vmexecError(_ errorString: String, cause: (any Error)? = nil) -> ContainerizationError {
let text = errorString.trimmingCharacters(in: .whitespacesAndNewlines)
let preserved: [ContainerizationError.Code] = [.notFound, .notExecutable]
let code = preserved.first { text.hasPrefix("\($0): ") } ?? .internalError
return ContainerizationError(code, message: "vmexec error: \(text)", cause: cause)
}

func setExit(_ status: Int32) {
self.state.withLock { state in
self.log.info(
Expand Down
2 changes: 2 additions & 0 deletions vminitd/Sources/VminitdCore/Server+GRPC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ extension ContainerizationError {
return .failedPrecondition
case .timeout:
return .deadlineExceeded
case .notExecutable:
return .permissionDenied
default:
return .internalError
}
Expand Down
23 changes: 18 additions & 5 deletions vminitd/Sources/vmexec/vmexec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ extension App {
}

guard FileManager.default.fileExists(atPath: resolvedExecutable.path) else {
throw App.Failure(message: "failed to find target executable \(executableArg)")
throw ContainerizationError(.notFound, message: "failed to find target executable \(executableArg)")
}
} else {
let path = Path.findPath(currentEnv) ?? Path.getCurrentPath()
guard let found = Path.lookPath(executableArg, path: path) else {
throw App.Failure(message: "failed to find target executable \(executableArg)")
throw ContainerizationError(.notFound, message: "failed to find target executable \(executableArg)")
}
resolvedExecutable = found
}
Expand All @@ -116,7 +116,20 @@ extension App {
}

guard execvpe(executable, argv, env) != -1 else {
throw App.Errno(stage: "execvpe(\(String(describing: executable)))", info: "failed to exec [\(process.args.joined(separator: " "))]")
let code: ContainerizationError.Code
switch errno {
case ENOENT:
code = .notFound
case EACCES, ENOEXEC:
code = .notExecutable
default:
code = .internalError
}
throw App.Errno(
code: code,
stage: "execvpe(\(String(describing: executable)))",
info: "failed to exec [\(process.args.joined(separator: " "))]"
)
}
fatalError("execvpe failed")
}
Expand Down Expand Up @@ -254,9 +267,9 @@ extension App {
}
}

static func Errno(stage: String, info: String = "") -> ContainerizationError {
static func Errno(code: ContainerizationError.Code = .internalError, stage: String, info: String = "") -> ContainerizationError {
let posix = POSIXError(.init(rawValue: errno)!, userInfo: ["stage": stage])
return ContainerizationError(.internalError, message: "\(info) \(String(describing: posix))")
return ContainerizationError(code, message: "\(info) \(String(describing: posix))")
}

static func Failure(message: String) -> ContainerizationError {
Expand Down