diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index e24bdf67a..1c12ff759 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -1134,8 +1134,7 @@ extension LinuxContainer { guard let vminitd = agent as? Vminitd else { throw ContainerizationError(.unsupported, message: "filesystemOperation requires Vminitd agent") } - let guestPath = URL(filePath: Self.guestRootfsPath(self.id)).appending(path: path).path - try await vminitd.filesystemOperation(operation: operation, path: guestPath) + try await vminitd.filesystemOperation(operation: operation, path: path, containerID: self.id) } } } diff --git a/Sources/Containerization/LinuxPod.swift b/Sources/Containerization/LinuxPod.swift index 6275a4d49..abbfe85e4 100644 --- a/Sources/Containerization/LinuxPod.swift +++ b/Sources/Containerization/LinuxPod.swift @@ -1285,8 +1285,7 @@ extension LinuxPod { guard let vminitd = agent as? Vminitd else { throw ContainerizationError(.unsupported, message: "filesystemOperation requires Vminitd agent") } - let guestPath = URL(filePath: Self.guestRootfsPath(containerID)).appending(path: path).path - try await vminitd.filesystemOperation(operation: operation, path: guestPath) + try await vminitd.filesystemOperation(operation: operation, path: path, containerID: containerID) } } } diff --git a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift index a091d8b25..e866f8c05 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift +++ b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift @@ -1154,6 +1154,15 @@ public nonisolated struct Com_Apple_Containerization_Sandbox_V3_FilesystemOperat set {operation = .thaw(newValue)} } + public var containerID: String { + get {_containerID ?? String()} + set {_containerID = newValue} + } + /// Returns true if `containerID` has been explicitly set. + public var hasContainerID: Bool {self._containerID != nil} + /// Clears the value of `containerID`. Subsequent reads from it will return its default value. + public mutating func clearContainerID() {self._containerID = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public nonisolated enum OneOf_Operation: Equatable, Sendable { @@ -1164,6 +1173,8 @@ public nonisolated struct Com_Apple_Containerization_Sandbox_V3_FilesystemOperat } public init() {} + + fileprivate var _containerID: String? = nil } public nonisolated struct Com_Apple_Containerization_Sandbox_V3_FilesystemOperationResponse: Sendable { @@ -3453,7 +3464,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_FiTrimResult: SwiftP nonisolated extension Com_Apple_Containerization_Sandbox_V3_FilesystemOperationRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".FilesystemOperationRequest" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}path\0\u{1}trim\0\u{1}freeze\0\u{1}thaw\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}path\0\u{1}trim\0\u{1}freeze\0\u{1}thaw\0\u{1}containerID\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -3501,6 +3512,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_FilesystemOperationR self.operation = .thaw(v) } }() + case 5: try { try decoder.decodeSingularStringField(value: &self._containerID) }() default: break } } @@ -3529,12 +3541,16 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_FilesystemOperationR }() case nil: break } + try { if let v = self._containerID { + try visitor.visitSingularStringField(value: v, fieldNumber: 5) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_FilesystemOperationRequest, rhs: Com_Apple_Containerization_Sandbox_V3_FilesystemOperationRequest) -> Bool { if lhs.path != rhs.path {return false} if lhs.operation != rhs.operation {return false} + if lhs._containerID != rhs._containerID {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Sources/Containerization/SandboxContext/SandboxContext.proto b/Sources/Containerization/SandboxContext/SandboxContext.proto index 24fb24256..100584a92 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.proto +++ b/Sources/Containerization/SandboxContext/SandboxContext.proto @@ -315,6 +315,7 @@ message FilesystemOperationRequest { FiFreezeParams freeze = 3; FiThawParams thaw = 4; } + optional string containerID = 5; } message FilesystemOperationResponse { diff --git a/Sources/Containerization/VirtualMachineAgent.swift b/Sources/Containerization/VirtualMachineAgent.swift index 05ea79505..1bbaf7839 100644 --- a/Sources/Containerization/VirtualMachineAgent.swift +++ b/Sources/Containerization/VirtualMachineAgent.swift @@ -41,7 +41,7 @@ public protocol VirtualMachineAgent: Sendable { /// Close any resources held by the agent. func close() async throws // Perform a filesystem operation on the given path. - func filesystemOperation(operation: FilesystemOperation, path: String) async throws + func filesystemOperation(operation: FilesystemOperation, path: String, containerID: String?) async throws // POSIX-y func getenv(key: String) async throws -> String diff --git a/Sources/Containerization/Vminitd.swift b/Sources/Containerization/Vminitd.swift index 6af76662e..7fded4432 100644 --- a/Sources/Containerization/Vminitd.swift +++ b/Sources/Containerization/Vminitd.swift @@ -213,11 +213,14 @@ extension Vminitd: VirtualMachineAgent { } /// Perform a filesystem operation on a path inside the sandbox's environment. - public func filesystemOperation(operation: FilesystemOperation, path: String) async throws { + public func filesystemOperation(operation: FilesystemOperation, path: String, containerID: String? = nil) async throws { _ = try await client.filesystemOperation( .with { $0.operation = operation.toProtoOperation() $0.path = path + if let containerID { + $0.containerID = containerID + } }) } diff --git a/vminitd/Sources/VminitdCore/Server+GRPC.swift b/vminitd/Sources/VminitdCore/Server+GRPC.swift index dd07ef54f..1a4ee5673 100644 --- a/vminitd/Sources/VminitdCore/Server+GRPC.swift +++ b/vminitd/Sources/VminitdCore/Server+GRPC.swift @@ -722,10 +722,21 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ async throws -> Com_Apple_Containerization_Sandbox_V3_FilesystemOperationResponse { let path = FilePath(request.path) + if !request.hasContainerID { + throw ContainerizationError( + .invalidArgument, + message: "containerID is required" + ) + } + + let container = try await state.get(container: request.containerID) + let containerPid = container.pid log.debug( "filesystemOperation", metadata: [ + "containerID": "\(request.containerID)", + "containerPid": "\(containerPid)", "operation": "\(String(describing: request.operation))", "path": "\(path)", ]) @@ -734,6 +745,58 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ throw RPCError(code: .invalidArgument, message: "path must be absolute") } + let selfMountFd = open("/proc/self/ns/mount", O_RDONLY | O_DIRECTORY | O_CLOEXEC) + if selfMountFd < 0 { + let error = swiftErrno("open") + throw RPCError(code: .internalError, message: "failed to open self mount namespace", cause: error) + } + + let containerMountFd = open("/proc/\(containerPid)/ns/mount", O_RDONLY | O_DIRECTORY | O_CLOEXEC) + if containerMountFd < 0 { + let error = swiftErrno("open") + throw RPCError(code: .internalError, message: "failed to open container mount namespace", cause: error) + } + + defer { + close(containerMountFd) + close(selfMountFd) + } + + var finfo = _stat_struct() + let selfMountStat = fstat(selfMountFd, &finfo) + if selfMountStat != 0 { + let error = swiftErrno("fstat") + throw RPCError(code: .internalError, message: "failed to stat self mount namespace", cause: error) + } + let selfInode = finfo.st_ino + + let containerMountStat = fstat(containerMountFd, &finfo) + if containerMountStat != 0 { + let error = swiftErrno("fstat") + throw RPCError(code: .internalError, message: "failed to stat container mount namespace", cause: error) + } + let containerInode = finfo.st_ino + + if selfInode == containerInode { + try doFilesystemOperation(path: path, operation: request.operation) + } else { + try await self.runOnDedicatedThread { + if unshare(CLONE_FS) != 0 { + let error = swiftErrno("unshare(CLONE_FS)") + throw RPCError(code: .internalError, message: "failed to unshare filesystem namespace", cause: error) + } + if setns(containerMountFd, CLONE_NEWNS) != 0 { + let error = swiftErrno("setns(CLONE_NEWNS)") + throw RPCError(code: .internalError, message: "failed to enter container mount namespace", cause: error) + } + try doFilesystemOperation(path: path, operation: request.operation) + } + } + + return .init() + } + + private func doFilesystemOperation(path: FilePath, operation: Com_Apple_Containerization_Sandbox_V3_FilesystemOperationRequest.Operation) throws { var finfo = _stat_struct() let rc = _stat(path.string, &finfo) if rc != 0 { @@ -753,7 +816,7 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ defer { close(fd) } do { - switch request.operation { + switch operation { case .freeze: try freezeFilesystem(fd: fd) case .thaw: @@ -1650,6 +1713,22 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ return error } + private func runOnDedicatedThread( + _ work: @escaping () throws -> T + ) async throws -> T { + try await withCheckedThrowingContinuation { continuation in + let thread = Thread { + do { + let result = try work() + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + thread.start() + } + } + // NOTE: This is just crummy. It works because today the assumption is // every NIC in the root net namespace is for the container(s), but if we // ever supported individual containers having their own NICs/IPs then this