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
5 changes: 5 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,11 @@ let package = Package(
],
path: "vminitd/Sources/VminitdCore"
),
.testTarget(
name: "VminitdCoreTests",
dependencies: ["VminitdCore"],
path: "Tests/VminitdCoreTests"
),
]
)

Expand Down
39 changes: 39 additions & 0 deletions Sources/Containerization/FilesystemOperationPath.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//

import ContainerizationError
import Foundation

package enum FilesystemOperationPath {
package static func validate(_ path: String) throws {
guard path.first == "/", !path.contains("\0") else {
throw invalidPath(path)
}
guard path == "/" || (!path.hasSuffix("/") && !path.contains("//")) else {
throw invalidPath(path)
}
guard !path.split(separator: "/").contains(where: { $0 == "." || $0 == ".." }) else {
throw invalidPath(path)
}
}

private static func invalidPath(_ path: String) -> ContainerizationError {
ContainerizationError(
.invalidArgument,
message: "filesystem operation path must be an absolute canonical container path: \(path.debugDescription)"
)
}
}
7 changes: 5 additions & 2 deletions Sources/Containerization/LinuxContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1134,8 +1134,11 @@ 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
)
}
}
}
Expand Down
18 changes: 16 additions & 2 deletions Sources/Containerization/LinuxPod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1262,8 +1262,19 @@ extension LinuxPod {
return try await fn(vm)
}

struct FilesystemOperationTarget: Equatable {
let containerID: String
let path: String
}

static func filesystemOperationTarget(containerID: String, path: String) throws -> FilesystemOperationTarget {
try FilesystemOperationPath.validate(path)
return FilesystemOperationTarget(containerID: containerID, path: path)
}

// Perform filesystem operations in a container.
public func filesystemOperation(_ containerID: String, operation: FilesystemOperation, path: String) async throws {
let target = try Self.filesystemOperationTarget(containerID: containerID, path: path)
try await self.state.withLock { state in
let createdState = try state.phase.createdState("filesystemOperation")

Expand All @@ -1285,8 +1296,11 @@ 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: target.path,
containerID: target.containerID
)
}
}
}
Expand Down
19 changes: 18 additions & 1 deletion Sources/Containerization/SandboxContext/SandboxContext.pb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,16 @@ public nonisolated struct Com_Apple_Containerization_Sandbox_V3_FilesystemOperat
set {operation = .thaw(newValue)}
}

/// Resolve path from this container's init process mount namespace.
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 {
Expand All @@ -1164,6 +1174,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 {
Expand Down Expand Up @@ -3453,7 +3465,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{3}container_id\0")

public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
Expand Down Expand Up @@ -3501,6 +3513,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_FilesystemOperationR
self.operation = .thaw(v)
}
}()
case 5: try { try decoder.decodeSingularStringField(value: &self._containerID) }()
default: break
}
}
Expand Down Expand Up @@ -3529,12 +3542,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
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/Containerization/SandboxContext/SandboxContext.proto
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ message FilesystemOperationRequest {
FiFreezeParams freeze = 3;
FiThawParams thaw = 4;
}
// Resolve path from this container's init process mount namespace.
optional string container_id = 5;
}

message FilesystemOperationResponse {
Expand Down
32 changes: 28 additions & 4 deletions Sources/Containerization/Vminitd.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,35 @@ extension Vminitd: VirtualMachineAgent {

/// Perform a filesystem operation on a path inside the sandbox's environment.
public func filesystemOperation(operation: FilesystemOperation, path: String) async throws {
try await filesystemOperation(operation: operation, path: path, containerID: nil)
}

public func filesystemOperation(
operation: FilesystemOperation,
path: String,
containerID: String?
) async throws {
_ = try await client.filesystemOperation(
.with {
$0.operation = operation.toProtoOperation()
$0.path = path
})
try Self.filesystemOperationRequest(
operation: operation,
path: path,
containerID: containerID
))
}

package static func filesystemOperationRequest(
operation: FilesystemOperation,
path: String,
containerID: String?
) throws -> Com_Apple_Containerization_Sandbox_V3_FilesystemOperationRequest {
try FilesystemOperationPath.validate(path)
return .with {
$0.operation = operation.toProtoOperation()
$0.path = path
if let containerID {
$0.containerID = containerID
}
}
}

public func createProcess(
Expand Down
45 changes: 44 additions & 1 deletion Tests/ContainerizationTests/LinuxContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
// limitations under the License.
//===----------------------------------------------------------------------===//

import ContainerizationError
import ContainerizationOCI
import ContainerizationOS
import Foundation
import Testing

Expand Down Expand Up @@ -119,4 +119,47 @@ struct LinuxContainerTests {
#expect(pod.maskedPaths == expectedMasked)
#expect(pod.readonlyPaths == expectedReadonly)
}

@Test func filesystemOperationPathAcceptsContainerAbsolutePaths() throws {
try FilesystemOperationPath.validate("/")
try FilesystemOperationPath.validate("/mnt/reclaim-data")
}

@Test(
arguments: [
"",
"mnt/reclaim-data",
"/mnt/../etc",
"/mnt/./reclaim-data",
"/mnt//reclaim-data",
"/mnt/reclaim-data/",
"/mnt/\0escape",
])
func filesystemOperationPathRejectsAmbiguousOrEscapingPaths(path: String) {
#expect(throws: ContainerizationError.self) {
try FilesystemOperationPath.validate(path)
}
}

@Test func podFilesystemOperationTargetsContainerMountNamespace() throws {
let target = try LinuxPod.filesystemOperationTarget(
containerID: "pod-clean-volume",
path: "/mnt/reclaim-data"
)

#expect(target.path == "/mnt/reclaim-data")
#expect(target.containerID == "pod-clean-volume")
}

@Test func filesystemOperationRequestTargetsContainerMountNamespace() throws {
let request = try Vminitd.filesystemOperationRequest(
operation: .trim,
path: "/mnt/reclaim-data",
containerID: "clean-volume-reclaim"
)

#expect(request.path == "/mnt/reclaim-data")
#expect(request.containerID == "clean-volume-reclaim")
#expect(request.hasContainerID)
}
}
Loading