From 390add40bfbbe0656816dd28139ad781a7e7a7b5 Mon Sep 17 00:00:00 2001 From: Simone Date: Thu, 30 Apr 2026 18:22:46 +0200 Subject: [PATCH 1/2] Add copyIn to resolve destination paths using Stat from vminitd --- Sources/Containerization/LinuxContainer.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index c51cb622..f3309451 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -1065,7 +1065,21 @@ extension LinuxContainer { } let isArchive = isDirectory.boolValue - let guestPath = URL(filePath: self.root).appending(path: destination.path) + let resolvedDestination: URL = try await state.vm.withAgent { agent in + guard let vminitd = agent as? Vminitd else { + throw ContainerizationError(.unsupported, message: "copyIn requires Vminitd agent") + } + do { + let stat = try await vminitd.stat(path: destination) + let isDir = (stat.mode & 0o170000) == 0o040000 + if isDir { + return destination.appendingPathComponent(source.lastPathComponent) + } + } catch { } + return destination + } + + let guestPath = URL(filePath: self.root).appending(path: resolvedDestination.path) let port = self.hostVsockPorts.wrappingAdd(1, ordering: .relaxed).oldValue let listener = try state.vm.listen(port) From 45b8ee5e83f690abb5bbafa7fb4adb4746428c7e Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 6 May 2026 06:28:43 +0200 Subject: [PATCH 2/2] Add resolveCopyInGuestPath for CopyIn --- Sources/Containerization/LinuxContainer.swift | 61 +++++++++++++++---- Sources/Containerization/Vminitd.swift | 7 ++- vminitd/Sources/VminitdCore/Server+GRPC.swift | 7 +++ 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index f3309451..a7f3607b 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -18,6 +18,7 @@ import ContainerizationArchive import ContainerizationError import ContainerizationExtras import ContainerizationOCI +import ContainerizationOS import Foundation import Logging import Synchronization @@ -1065,21 +1066,19 @@ extension LinuxContainer { } let isArchive = isDirectory.boolValue - let resolvedDestination: URL = try await state.vm.withAgent { agent in + let guestPath: URL = try await state.vm.withAgent { agent in guard let vminitd = agent as? Vminitd else { throw ContainerizationError(.unsupported, message: "copyIn requires Vminitd agent") } - do { - let stat = try await vminitd.stat(path: destination) - let isDir = (stat.mode & 0o170000) == 0o040000 - if isDir { - return destination.appendingPathComponent(source.lastPathComponent) - } - } catch { } - return destination + + return try await self.resolveCopyInGuestPath( + from: source, + to: destination, + sourceIsDirectory: isArchive, + using: vminitd + ) } - - let guestPath = URL(filePath: self.root).appending(path: resolvedDestination.path) + let port = self.hostVsockPorts.wrappingAdd(1, ordering: .relaxed).oldValue let listener = try state.vm.listen(port) @@ -1164,6 +1163,46 @@ extension LinuxContainer { } } + private func resolveCopyInGuestPath( + from source: URL, + to destination: URL, + sourceIsDirectory: Bool, + using vminitd: Vminitd + ) async throws -> URL { + let guestDestination = URL(filePath: self.root).appending(path: destination.path) + + let stat: ContainerizationOS.Stat? + do { + stat = try await vminitd.stat(path: guestDestination) + } catch let error as ContainerizationError where error.code == .notFound { + stat = nil + } + // Any other error propagates so transport and permission failures are visible. + + guard let stat else { + if destination.hasDirectoryPath && !sourceIsDirectory { + throw ContainerizationError( + .invalidArgument, + message: "destination directory does not exist: \(destination.path)" + ) + } + return guestDestination + } + + let destinationIsDirectory = (stat.mode & UInt32(S_IFMT)) == UInt32(S_IFDIR) + guard destinationIsDirectory else { + if sourceIsDirectory { + throw ContainerizationError( + .invalidArgument, + message: "cannot copy directory over existing file: \(destination.path)" + ) + } + return guestDestination + } + + return guestDestination.appendingPathComponent(source.lastPathComponent) + } + /// Copy a file or directory from the container to the host. /// /// Data transfer happens over a dedicated vsock connection. For directories, diff --git a/Sources/Containerization/Vminitd.swift b/Sources/Containerization/Vminitd.swift index bf62a034..d23eff53 100644 --- a/Sources/Containerization/Vminitd.swift +++ b/Sources/Containerization/Vminitd.swift @@ -472,7 +472,12 @@ extension Vminitd { $0.path = path.path } - let response = try await client.stat(request) + let response: Com_Apple_Containerization_Sandbox_V3_StatResponse + do { + response = try await client.stat(request) + } catch let error as RPCError where error.code == .notFound { + throw ContainerizationError(.notFound, message: "stat: path not found '\(path.path)'", cause: error) + } guard response.error.isEmpty else { throw ContainerizationError(.internalError, message: "stat: \(response.error)") } diff --git a/vminitd/Sources/VminitdCore/Server+GRPC.swift b/vminitd/Sources/VminitdCore/Server+GRPC.swift index 23cca9f8..c035623d 100644 --- a/vminitd/Sources/VminitdCore/Server+GRPC.swift +++ b/vminitd/Sources/VminitdCore/Server+GRPC.swift @@ -384,6 +384,13 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ let result = _stat(request.path, &s) if result == -1 { let error = swiftErrno("stat") + if error.code == .ENOENT { + throw RPCError( + code: .notFound, + message: "stat: path not found '\(request.path)'", + cause: error + ) + } return .with { $0.error = "\(error)" } } return .with {