From 8553a754e3db523711ed87a3f7cd081811b5bdb2 Mon Sep 17 00:00:00 2001 From: Jaewon Hur Date: Wed, 19 Nov 2025 10:25:44 -0500 Subject: [PATCH 1/3] Accept file mount and create virtiofs directory for shared files Create 'virtiofs' directory under container root and hardlink all mount requested files to that directory. Then, feed that directory to Linux VM through VirtIO device. --- .../Containerization/AttachedFilesystem.swift | 12 +++++++++++- .../Containerization/ContainerManager.swift | 18 ++++++++++++++++++ Sources/Containerization/Mount.swift | 12 +++++++++++- .../VZVirtualMachineInstance.swift | 10 ++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/Sources/Containerization/AttachedFilesystem.swift b/Sources/Containerization/AttachedFilesystem.swift index ebe42ab5..20fc1a6c 100644 --- a/Sources/Containerization/AttachedFilesystem.swift +++ b/Sources/Containerization/AttachedFilesystem.swift @@ -16,6 +16,7 @@ import ContainerizationExtras import ContainerizationOCI +import Foundation /// A filesystem that was attached and able to be mounted inside the runtime environment. public struct AttachedFilesystem: Sendable { @@ -32,7 +33,16 @@ public struct AttachedFilesystem: Sendable { public init(mount: Mount, allocator: any AddressAllocator) throws { switch mount.type { case "virtiofs": - let name = try hashMountSource(source: mount.source) + let name: String = try { + guard mount.isFile else { + return try hashMountSource(source: mount.source) + } + + let file = URL(fileURLWithPath: mount.source).lastPathComponent + let directory = try hashMountSource(source: mount.hardlinkDirectory!) + + return URL(string: directory)!.appendingPathComponent(file).path + }() self.source = name case "ext4": let char = try allocator.allocate() diff --git a/Sources/Containerization/ContainerManager.swift b/Sources/Containerization/ContainerManager.swift index 72de285d..eac3ce02 100644 --- a/Sources/Containerization/ContainerManager.swift +++ b/Sources/Containerization/ContainerManager.swift @@ -419,6 +419,18 @@ public struct ContainerManager: Sendable { } config.bootLog = BootLog.file(path: self.containerRoot.appendingPathComponent(id).appendingPathComponent("bootlog.log")) try configuration(&config) + + let sharedDir = sharedFileDirectory(id) + for i in config.mounts.indices { + if config.mounts[i].isFile { + let file = URL(fileURLWithPath: config.mounts[i].source) + let hardlink = sharedDir.appendingPathComponent(file.lastPathComponent) + + try FileManager.default.linkItem(at: file, to: hardlink) + config.mounts[i].hardlinkDirectory = sharedDir.path + config.mounts[i].options.append("bind") + } + } } } @@ -433,9 +445,15 @@ public struct ContainerManager: Sendable { private func createContainerRoot(_ id: String) throws -> URL { let path = containerRoot.appendingPathComponent(id) try FileManager.default.createDirectory(at: path, withIntermediateDirectories: false) + try FileManager.default.createDirectory(at: sharedFileDirectory(id), withIntermediateDirectories: false) + return path } + private func sharedFileDirectory(_ id: String) -> URL { + containerRoot.appendingPathComponent(id).appendingPathComponent("virtiofs") + } + private func unpack(image: Image, destination: URL, size: UInt64) async throws -> Mount { do { let unpacker = EXT4Unpacker(blockSizeInBytes: size) diff --git a/Sources/Containerization/Mount.swift b/Sources/Containerization/Mount.swift index 6aae7f0a..3829cfef 100644 --- a/Sources/Containerization/Mount.swift +++ b/Sources/Containerization/Mount.swift @@ -36,6 +36,8 @@ public struct Mount: Sendable { /// should create for this specific mount (virtioblock /// virtiofs etc.). public let runtimeOptions: RuntimeOptions + /// hardlink directory path if the source is file. + public var hardlinkDirectory: String? /// A type representing a "hint" of what type /// of mount this really is (block, directory, purely @@ -144,7 +146,15 @@ extension Mount { throw ContainerizationError(.notFound, message: "directory \(source) does not exist") } - let name = try hashMountSource(source: self.source) + let source = isFile ? self.hardlinkDirectory! : self.source + let name = try hashMountSource(source: source) + guard + !config.directorySharingDevices.contains( + where: { ($0 as? VZVirtioFileSystemDeviceConfiguration)?.tag == name }) + else { + break + } + let urlSource = URL(fileURLWithPath: source) let device = VZVirtioFileSystemDeviceConfiguration(tag: name) diff --git a/Sources/Containerization/VZVirtualMachineInstance.swift b/Sources/Containerization/VZVirtualMachineInstance.swift index 3e1baa30..7735b121 100644 --- a/Sources/Containerization/VZVirtualMachineInstance.swift +++ b/Sources/Containerization/VZVirtualMachineInstance.swift @@ -423,6 +423,16 @@ extension Mount { var isBlock: Bool { type == "ext4" } + + var isFile: Bool { + guard self.type == "virtiofs" else { + return false + } + + var isDirectory: ObjCBool = false + let exists = FileManager.default.fileExists(atPath: self.source, isDirectory: &isDirectory) + return exists && !isDirectory.boolValue + } } extension Kernel { From ad139db19a5e7f310a8aaf7ad9baa4009f22ac0c Mon Sep 17 00:00:00 2001 From: Jaewon Hur Date: Wed, 19 Nov 2025 11:03:05 -0500 Subject: [PATCH 2/3] Mount shared files under target paths Mount shared file directory under bundlePath, and mount each file under shared file directory to corresponding target path again. --- Sources/ContainerizationOS/Mount/Mount.swift | 29 ++++++++++++++++++-- vminitd/Sources/vmexec/Mount.swift | 27 ++++++++++++++---- vminitd/Sources/vmexec/RunCommand.swift | 20 ++++++++++++-- 3 files changed, 65 insertions(+), 11 deletions(-) diff --git a/Sources/ContainerizationOS/Mount/Mount.swift b/Sources/ContainerizationOS/Mount/Mount.swift index e62bb9f0..2b7a30c4 100644 --- a/Sources/ContainerizationOS/Mount/Mount.swift +++ b/Sources/ContainerizationOS/Mount/Mount.swift @@ -115,6 +115,10 @@ extension Mount { return false } + public var isSharedFile: Bool { + type == "" && self.options.contains("bind") + } + /// Mount the mount relative to `root` with the current set of data in the object. /// Optionally provide `createWithPerms` to set the permissions for the directory that /// it will be mounted at. @@ -146,12 +150,25 @@ extension Mount { // Ensure propagation type change flags aren't included in other calls. let originalFlags = opts.flags & ~(propagationTypes) - let targetURL = URL(fileURLWithPath: self.target) + // TODO: What are lines 153, 154 for? + // let targetURL = URL(fileURLWithPath: self.target) + // let targetParent = targetURL.deletingLastPathComponent().path + // if let perms = createWithPerms { + // try mkdirAll(targetParent, perms) + // } + + let targetURL = URL(fileURLWithPath: target) let targetParent = targetURL.deletingLastPathComponent().path if let perms = createWithPerms { try mkdirAll(targetParent, perms) } - try mkdirAll(target, 0o755) + + if self.isSharedFile { + try mkdirAll(targetParent, 0o755) + createFile(target) + } else { + try mkdirAll(target, 0o755) + } if opts.flags & Int32(MS_REMOUNT) == 0 || !dataString.isEmpty { guard _mount(self.source, target, self.type, UInt(originalFlags), dataString) == 0 else { @@ -186,6 +203,14 @@ extension Mount { ) } + private func createFile(_ name: String) { + _ = FileManager.default.createFile( + atPath: name, + contents: nil, + attributes: nil + ) + } + private func parseMountOptions() -> MountOptions { var mountOpts = MountOptions() for option in self.options { diff --git a/vminitd/Sources/vmexec/Mount.swift b/vminitd/Sources/vmexec/Mount.swift index 0a269065..806e0c76 100644 --- a/vminitd/Sources/vmexec/Mount.swift +++ b/vminitd/Sources/vmexec/Mount.swift @@ -22,15 +22,17 @@ import Musl struct ContainerMount { private let mounts: [ContainerizationOCI.Mount] private let rootfs: String + private let sharedFileDirectory: URL - init(rootfs: String, mounts: [ContainerizationOCI.Mount]) { + init(rootfs: String, sharedFileDirectory: URL, mounts: [ContainerizationOCI.Mount]) { self.rootfs = rootfs + self.sharedFileDirectory = sharedFileDirectory self.mounts = mounts } func mountToRootfs() throws { for m in self.mounts { - let osMount = m.toOSMount() + let osMount = m.toOSMount(sharedFileDirectory) try osMount.mount(root: self.rootfs) } } @@ -55,10 +57,23 @@ struct ContainerMount { } extension ContainerizationOCI.Mount { - func toOSMount() -> ContainerizationOS.Mount { - ContainerizationOS.Mount( - type: self.type, - source: self.source, + var isSharedFile: Bool { + type == "virtiofs" && options.contains("bind") + } + + func toOSMount(_ sharedFileDirectory: URL) -> ContainerizationOS.Mount { + let type = isSharedFile ? "" : self.type + let source = { + guard isSharedFile else { + return self.source + } + let name = URL(string: self.source)!.lastPathComponent + return sharedFileDirectory.appendingPathComponent(name).path + }() + + return ContainerizationOS.Mount( + type: type, + source: source, target: self.destination, options: self.options ) diff --git a/vminitd/Sources/vmexec/RunCommand.swift b/vminitd/Sources/vmexec/RunCommand.swift index f4133ce5..478fab06 100644 --- a/vminitd/Sources/vmexec/RunCommand.swift +++ b/vminitd/Sources/vmexec/RunCommand.swift @@ -41,9 +41,12 @@ struct RunCommand: ParsableCommand { } private func childRootSetup(rootfs: ContainerizationOCI.Root, mounts: [ContainerizationOCI.Mount], log: Logger) throws { + let sharedFileDirectory = URL(fileURLWithPath: bundlePath).appendingPathComponent("virtiofs") + // setup rootfs try prepareRoot(rootfs: rootfs.path) - try mountRootfs(rootfs: rootfs.path, mounts: mounts) + try prepareSharedFiles(sharedFileDirectory: sharedFileDirectory, mounts: mounts) + try mountRootfs(rootfs: rootfs.path, sharedFileDirectory: sharedFileDirectory, mounts: mounts) try setDevSymlinks(rootfs: rootfs.path) try pivotRoot(rootfs: rootfs.path) @@ -180,8 +183,8 @@ struct RunCommand: ParsableCommand { } } - private func mountRootfs(rootfs: String, mounts: [ContainerizationOCI.Mount]) throws { - let containerMount = ContainerMount(rootfs: rootfs, mounts: mounts) + private func mountRootfs(rootfs: String, sharedFileDirectory: URL, mounts: [ContainerizationOCI.Mount]) throws { + let containerMount = ContainerMount(rootfs: rootfs, sharedFileDirectory: sharedFileDirectory, mounts: mounts) try containerMount.mountToRootfs() try containerMount.configureConsole() } @@ -196,6 +199,17 @@ struct RunCommand: ParsableCommand { } } + private func prepareSharedFiles(sharedFileDirectory: URL, mounts: [ContainerizationOCI.Mount]) throws { + if let source = mounts.first(where: { $0.isSharedFile })?.source { + let tag = URL(string: source)!.deletingLastPathComponent().path + + try FileManager.default.createDirectory(at: sharedFileDirectory, withIntermediateDirectories: false) + guard mount(tag, sharedFileDirectory.path, "virtiofs", UInt(0), nil) == 0 else { + throw App.Errno(stage: "mount(shared)") + } + } + } + private func setDevSymlinks(rootfs: String) throws { let links: [(src: String, dst: String)] = [ ("/proc/self/fd", "/dev/fd"), From ece90403776e4323846e36bced1ac255b8d7d1df Mon Sep 17 00:00:00 2001 From: Jaewon Hur Date: Wed, 19 Nov 2025 11:12:15 -0500 Subject: [PATCH 3/3] Use path hash as the identifier of shared file Avoid conflict from same file name. --- Sources/Containerization/AttachedFilesystem.swift | 4 ++-- Sources/Containerization/ContainerManager.swift | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/Containerization/AttachedFilesystem.swift b/Sources/Containerization/AttachedFilesystem.swift index 20fc1a6c..8c3d1bae 100644 --- a/Sources/Containerization/AttachedFilesystem.swift +++ b/Sources/Containerization/AttachedFilesystem.swift @@ -38,10 +38,10 @@ public struct AttachedFilesystem: Sendable { return try hashMountSource(source: mount.source) } - let file = URL(fileURLWithPath: mount.source).lastPathComponent + let fileTag = try hashMountSource(source: mount.source) let directory = try hashMountSource(source: mount.hardlinkDirectory!) - return URL(string: directory)!.appendingPathComponent(file).path + return URL(string: directory)!.appendingPathComponent(fileTag).path }() self.source = name case "ext4": diff --git a/Sources/Containerization/ContainerManager.swift b/Sources/Containerization/ContainerManager.swift index eac3ce02..9e14b3d2 100644 --- a/Sources/Containerization/ContainerManager.swift +++ b/Sources/Containerization/ContainerManager.swift @@ -424,7 +424,8 @@ public struct ContainerManager: Sendable { for i in config.mounts.indices { if config.mounts[i].isFile { let file = URL(fileURLWithPath: config.mounts[i].source) - let hardlink = sharedDir.appendingPathComponent(file.lastPathComponent) + let fileTag = try hashMountSource(source: config.mounts[i].source) + let hardlink = sharedDir.appendingPathComponent(fileTag) try FileManager.default.linkItem(at: file, to: hardlink) config.mounts[i].hardlinkDirectory = sharedDir.path