diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index 9033a206..71494da8 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -671,7 +671,7 @@ extension LinuxContainer { )) } - spec.mounts = mounts + spec.mounts = sortMountsByDestinationDepth(mounts) let stdio = IOUtil.setup( portAllocator: self.hostVsockPorts, @@ -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, diff --git a/Sources/Containerization/LinuxPod.swift b/Sources/Containerization/LinuxPod.swift index 7574d697..8e9fd8f6 100644 --- a/Sources/Containerization/LinuxPod.swift +++ b/Sources/Containerization/LinuxPod.swift @@ -558,7 +558,7 @@ extension LinuxPod { )) } - spec.mounts = mounts + spec.mounts = sortMountsByDestinationDepth(mounts) // Configure namespaces for the container var namespaces: [LinuxNamespace] = [ diff --git a/Sources/Integration/ContainerTests.swift b/Sources/Integration/ContainerTests.swift index 97f5866e..cdf4953b 100644 --- a/Sources/Integration/ContainerTests.swift +++ b/Sources/Integration/ContainerTests.swift @@ -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 ?? "")'") + } + } } diff --git a/Sources/Integration/Suite.swift b/Sources/Integration/Suite.swift index 2f0a079d..b1d2bc01 100644 --- a/Sources/Integration/Suite.swift +++ b/Sources/Integration/Suite.swift @@ -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), diff --git a/Tests/ContainerizationTests/MountTests.swift b/Tests/ContainerizationTests/MountTests.swift index 908204b6..a0f5f493 100644 --- a/Tests/ContainerizationTests/MountTests.swift +++ b/Tests/ContainerizationTests/MountTests.swift @@ -14,6 +14,7 @@ // limitations under the License. //===----------------------------------------------------------------------===// +import ContainerizationOCI import Foundation import Testing @@ -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"]) + } }