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: 4 additions & 1 deletion Sources/Containerization/Hash.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@

import Crypto
import ContainerizationError
import Foundation

public func hashMountSource(source: String) throws -> String {
guard let data = source.data(using: .utf8) else {
// Resolve symlinks so different paths to the same directory get the same hash.
let resolvedSource = URL(fileURLWithPath: source).resolvingSymlinksInPath().path
guard let data = resolvedSource.data(using: .utf8) else {
throw ContainerizationError(.invalidArgument, message: "\(source) could not be converted to Data")
}
return String(SHA256.hash(data: data).encoded.prefix(36))
Expand Down
11 changes: 11 additions & 0 deletions Sources/Containerization/VZVirtualMachineInstance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,19 @@ extension VZVirtualMachineInstance.Configuration {
config.bootLoader = loader

try initialFilesystem.configure(config: &config)

// Track used virtiofs tags to avoid creating duplicate VZ devices.
// The same source directory mounted to multiple destinations shares one device.
var usedVirtioFSTags: Set<String> = []
for (_, mounts) in self.mountsByID {
for mount in mounts {
if case .virtiofs = mount.runtimeOptions {
let tag = try hashMountSource(source: mount.source)
if usedVirtioFSTags.contains(tag) {
continue
}
usedVirtioFSTags.insert(tag)
}
try mount.configure(config: &config)
}
}
Expand Down
135 changes: 135 additions & 0 deletions Sources/Integration/ContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2452,4 +2452,139 @@ extension IntegrationSuite {
throw error
}
}

func testDuplicateVirtiofsMount() async throws {
let id = "test-duplicate-virtiofs-mount"

let bs = try await bootstrap(id)

// Create a temp directory with a file
let sharedDir = FileManager.default.uniqueTemporaryDirectory(create: true)
try "shared content".write(to: sharedDir.appendingPathComponent("data.txt"), atomically: true, encoding: .utf8)

let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.arguments = ["sleep", "100"]
// Mount the same source directory to two different destinations
config.mounts.append(.share(source: sharedDir.path, destination: "/mnt1"))
config.mounts.append(.share(source: sharedDir.path, destination: "/mnt2"))
config.bootLog = bs.bootLog
}

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

// Verify both mounts work. Read from /mnt1, then /mnt2
let exec1 = try await container.exec("read-mnt1") { config in
config.arguments = ["cat", "/mnt1/data.txt"]
config.stdout = buffer1
}
try await exec1.start()
var status = try await exec1.wait()
try await exec1.delete()

guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "read from /mnt1 failed with status \(status)")
}

guard String(data: buffer1.data, encoding: .utf8) == "shared content" else {
throw IntegrationError.assert(msg: "unexpected content from /mnt1")
}

let exec2 = try await container.exec("read-mnt2") { config in
config.arguments = ["cat", "/mnt2/data.txt"]
config.stdout = buffer2
}
try await exec2.start()
status = try await exec2.wait()
try await exec2.delete()

guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "read from /mnt2 failed with status \(status)")
}

guard String(data: buffer2.data, encoding: .utf8) == "shared content" else {
throw IntegrationError.assert(msg: "unexpected content from /mnt2")
}

try await container.kill(SIGKILL)
try await container.wait()
try await container.stop()
} catch {
try? await container.stop()
throw error
}
}

func testDuplicateVirtiofsMountViaSymlink() async throws {
let id = "test-duplicate-virtiofs-mount-symlink"

let bs = try await bootstrap(id)

// Create a temp directory with a file, and a symlink to the same directory
let tempDir = FileManager.default.uniqueTemporaryDirectory(create: true)
let realDir = tempDir.appendingPathComponent("realdir")
let symlinkDir = tempDir.appendingPathComponent("symlinkdir")

try FileManager.default.createDirectory(at: realDir, withIntermediateDirectories: true)
try "symlink test content".write(to: realDir.appendingPathComponent("file.txt"), atomically: true, encoding: .utf8)
try FileManager.default.createSymbolicLink(at: symlinkDir, withDestinationURL: realDir)

let buffer1 = BufferWriter()
let buffer2 = BufferWriter()
let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in
config.process.arguments = ["sleep", "100"]
config.mounts.append(.share(source: realDir.path, destination: "/mnt1"))
config.mounts.append(.share(source: symlinkDir.path, destination: "/mnt2"))
config.bootLog = bs.bootLog
}

do {
// This should succeed as the symlink should resolve to the same directory
try await container.create()
try await container.start()

let exec1 = try await container.exec("read-mnt1") { config in
config.arguments = ["cat", "/mnt1/file.txt"]
config.stdout = buffer1
}
try await exec1.start()
var status = try await exec1.wait()
try await exec1.delete()

guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "read from /mnt1 failed with status \(status)")
}

guard String(data: buffer1.data, encoding: .utf8) == "symlink test content" else {
throw IntegrationError.assert(msg: "unexpected content from /mnt1")
}

// Verify mount via symlink works now
let exec2 = try await container.exec("read-mnt2") { config in
config.arguments = ["cat", "/mnt2/file.txt"]
config.stdout = buffer2
}
try await exec2.start()
status = try await exec2.wait()
try await exec2.delete()

guard status.exitCode == 0 else {
throw IntegrationError.assert(msg: "read from /mnt2 failed with status \(status)")
}

guard String(data: buffer2.data, encoding: .utf8) == "symlink test content" else {
throw IntegrationError.assert(msg: "unexpected content from /mnt2")
}

try await container.kill(SIGKILL)
try await container.wait()
try await container.stop()
} catch {
try? await container.stop()
throw error
}
}
}
2 changes: 2 additions & 0 deletions Sources/Integration/Suite.swift
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@ struct IntegrationSuite: AsyncParsableCommand {
Test("container rlimit open files", testRLimitOpenFiles),
Test("container rlimit multiple", testRLimitMultiple),
Test("container rlimit exec", testRLimitExec),
Test("container duplicate virtiofs mount", testDuplicateVirtiofsMount),
Test("container duplicate virtiofs mount via symlink", testDuplicateVirtiofsMountViaSymlink),

// Pods
Test("pod single container", testPodSingleContainer),
Expand Down