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
12 changes: 11 additions & 1 deletion Sources/Containerization/LinuxContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@ extension LinuxContainer {
))
}

spec.mounts = mounts
spec.mounts = sortMountsByDestinationDepth(mounts)

let stdio = IOUtil.setup(
portAllocator: self.hostVsockPorts,
Expand Down Expand Up @@ -1292,6 +1292,16 @@ extension AttachedFilesystem {
}
}

/// Sort mounts by the depth of their destination path. This ensures that
/// higher level mounts don't shadow other mounts. For example, if a user
/// specifies mounts for `/tmp/foo/bar` and `/tmp`, sorting by depth ensures
/// `/tmp` is mounted first without shadowing `/tmp/foo/bar`.
func sortMountsByDestinationDepth(_ mounts: [ContainerizationOCI.Mount]) -> [ContainerizationOCI.Mount] {
mounts.sorted { a, b in
a.destination.split(separator: "/").count < b.destination.split(separator: "/").count
}
}

struct IOUtil {
static func setup(
portAllocator: borrowing Atomic<UInt32>,
Expand Down
2 changes: 1 addition & 1 deletion Sources/Containerization/LinuxPod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ extension LinuxPod {
))
}

spec.mounts = mounts
spec.mounts = sortMountsByDestinationDepth(mounts)

// Configure namespaces for the container
var namespaces: [LinuxNamespace] = [
Expand Down
45 changes: 45 additions & 0 deletions Sources/Integration/ContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4344,4 +4344,49 @@ extension IntegrationSuite {
throw error
}
}

// Verify that mounts are sorted by destination path depth so that a
// higher-level mount (e.g. /mnt) doesn't shadow a deeper mount
// (e.g. /mnt/deep/nested). Both directories are separate virtiofs
// shares; the sort ensures /mnt is mounted first and /mnt/deep/nested
// on top of it.
func testMountsSortedByDepth() async throws {
let id = "test-mount-sort-depth"

let bs = try await bootstrap(id)
let buffer = BufferWriter()

// Create two separate mount directories with distinct files.
let deepDir = FileManager.default.uniqueTemporaryDirectory(create: true)
try "deep-content".write(to: deepDir.appendingPathComponent("deep.txt"), atomically: true, encoding: .utf8)

let shallowDir = FileManager.default.uniqueTemporaryDirectory(create: true)
try "shallow-content".write(to: shallowDir.appendingPathComponent("shallow.txt"), atomically: true, encoding: .utf8)

// Add deeper mount first, then shallower mount. Without sorting the
// shallower mount would shadow the deeper one.
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.arguments = ["/bin/cat", "/mnt/deep/nested/deep.txt"]
config.mounts.append(.share(source: deepDir.path, destination: "/mnt/deep/nested"))
config.mounts.append(.share(source: shallowDir.path, destination: "/mnt"))
config.process.stdout = buffer
config.bootLog = bs.bootLog
}

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

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

guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "process status \(status) != 0")
}

let value = String(data: buffer.data, encoding: .utf8)
guard value == "deep-content" else {
throw IntegrationError.assert(
msg: "expected 'deep-content' but got '\(value ?? "<nil>")'")
}
}
}
1 change: 1 addition & 0 deletions Sources/Integration/Suite.swift
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ struct IntegrationSuite: AsyncParsableCommand {
Test("container noNewPrivileges exec", testNoNewPrivilegesExec),
Test("container workingDir created", testWorkingDirCreated),
Test("container workingDir exec created", testWorkingDirExecCreated),
Test("container mount sort by depth", testMountsSortedByDepth),

// Pods
Test("pod single container", testPodSingleContainer),
Expand Down
44 changes: 44 additions & 0 deletions Tests/ContainerizationTests/MountTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// limitations under the License.
//===----------------------------------------------------------------------===//

import ContainerizationOCI
import Foundation
import Testing

Expand All @@ -40,4 +41,47 @@ struct MountTests {
#expect(Bool(false), "Expected virtiofs runtime options")
}
}

@Test func sortMountsByDestinationDepthPreventsParentShadowing() {
let mounts: [ContainerizationOCI.Mount] = [
.init(destination: "/tmp/foo/bar"),
.init(destination: "/tmp"),
.init(destination: "/var/log/app"),
.init(destination: "/var"),
]

let sorted = sortMountsByDestinationDepth(mounts)

#expect(
sorted.map(\.destination) == [
"/tmp",
"/var",
"/tmp/foo/bar",
"/var/log/app",
])
}

@Test func sortMountsByDestinationDepthPreservesOrderForEqualDepth() {
let mounts: [ContainerizationOCI.Mount] = [
.init(destination: "/b"),
.init(destination: "/a"),
.init(destination: "/c"),
]

let sorted = sortMountsByDestinationDepth(mounts)

// All same depth, order should be preserved (stable sort).
#expect(sorted.map(\.destination) == ["/b", "/a", "/c"])
}

@Test func sortMountsByDestinationDepthHandlesTrailingAndDoubleSlashes() {
let mounts: [ContainerizationOCI.Mount] = [
.init(destination: "/a//b/c"),
.init(destination: "/a/"),
]

let sorted = sortMountsByDestinationDepth(mounts)

#expect(sorted.map(\.destination) == ["/a/", "/a//b/c"])
}
}
Loading