Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a20c3f9
Add file mount detection and parent directory sharing
Sunsvea Aug 6, 2025
429a379
Implement bind mount functionality for single files
Sunsvea Aug 6, 2025
eb4a1ee
Add tests for single file mount detection
Sunsvea Aug 6, 2025
6a32cdf
Fix mounts property reference in bind mount logic
Sunsvea Aug 6, 2025
dedb89f
Fix AttachedFilesystem property access in mount logic
Sunsvea Aug 6, 2025
d9f7afe
Fix allocator usage in mount tests
Sunsvea Aug 6, 2025
3f81e5f
Run formatter
Sunsvea Aug 6, 2025
dd0b78a
Merge branch 'main' into feature/single-file-mount-support
Sunsvea Aug 6, 2025
2f5c7b2
Address reviewer feedback: rename isFileBind to isFile and revert mou…
Sunsvea Aug 6, 2025
b6730f6
Revert rootfs mount to use original .to pattern for consistency
Sunsvea Aug 6, 2025
9252ef5
Revert spec.mounts to use clean .to pattern instead of verbose .init
Sunsvea Aug 6, 2025
0e23a8c
Add integration test coverage for single file mount support and impro…
Sunsvea Aug 6, 2025
75fd986
Implement hardlink-based file isolation for single file mounts
Sunsvea Aug 7, 2025
0b2e6d3
Remove bind mount logic as hardlinked files are directly accessible
Sunsvea Aug 7, 2025
ca23072
Update AttachedFilesystem to use deterministic hardlink isolation and…
Sunsvea Aug 7, 2025
d117e5a
Add comprehensive unit tests for hardlink isolation and fix Foundatio…
Sunsvea Aug 7, 2025
22aa0d9
Apply code formatting to hardlink isolation implementation
Sunsvea Aug 7, 2025
29f5518
Merge branch 'main' into feature/single-file-mount-support
Sunsvea Aug 7, 2025
d6ad5e7
feat: add mount consolidation for multiple single file mounts
Sunsvea Aug 11, 2025
eb9decd
Merge branch 'main' into feature/single-file-mount-support
Sunsvea Aug 11, 2025
7d2aba9
feat: add security hardening for file mount isolation with atomic ope…
Sunsvea Aug 11, 2025
9104a3d
fix: add race condition protection to createIsolatedFileShare()
Sunsvea Aug 15, 2025
ce04bc1
fix: prevent temp directory collisions in createIsolatedFileShare()
Sunsvea Aug 15, 2025
2e8a5fe
Merge branch 'origin/main' into feature/single-file-mount-support
Sunsvea Aug 15, 2025
0107d82
fix: resolve race condition in Mount tests with UUID-based temp direc…
Sunsvea Aug 15, 2025
648a7ba
chore: run code formatter
Sunsvea Aug 15, 2025
b09860c
fix: resolve race conditions in tests when executed in parallel
Sunsvea Aug 15, 2025
55cbcac
chore: run code formatter
Sunsvea Aug 15, 2025
07ee4ff
refactor: remove redundant caching mechanism
Sunsvea Aug 15, 2025
0636693
fix: filename conflicts in tests when run in parallel
Sunsvea Aug 18, 2025
442718a
Merge branch 'main' into feature/single-file-mount-support
Sunsvea Aug 18, 2025
c8b5a9d
Merge branch 'main' into feature/single-file-mount-support
Sunsvea Sep 11, 2025
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
21 changes: 19 additions & 2 deletions Sources/Containerization/AttachedFilesystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -27,12 +28,22 @@ public struct AttachedFilesystem: Sendable {
public var destination: String
/// The options to use when mounting the filesystem.
public var options: [String]
/// True if this is a single file mount using hardlink isolation
var isFile: Bool

#if os(macOS)
public init(mount: Mount, allocator: any AddressAllocator<Character>) throws {
self.isFile = mount.isFile

switch mount.type {
case "virtiofs":
let name = try hashMountSource(source: mount.source)
let shareSource: String
if mount.isFile {
shareSource = try mount.createIsolatedFileShare()
} else {
shareSource = mount.source
}
let name = try hashMountSource(source: shareSource)
self.source = name
case "ext4":
let char = try allocator.allocate()
Expand All @@ -42,7 +53,13 @@ public struct AttachedFilesystem: Sendable {
}
self.type = mount.type
self.options = mount.options
self.destination = mount.destination

// For file mounts with hardlink isolation, mount at parent directory
if mount.isFile && mount.type == "virtiofs" {
self.destination = URL(fileURLWithPath: mount.destination).deletingLastPathComponent().path
} else {
self.destination = mount.destination
}
}
#endif
}
127 changes: 124 additions & 3 deletions Sources/Containerization/Mount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,120 @@ public struct Mount: Sendable {
#if os(macOS)

extension Mount {
var isFile: Bool {
var isDirectory: ObjCBool = false
let exists = FileManager.default.fileExists(atPath: self.source, isDirectory: &isDirectory)
return exists && !isDirectory.boolValue
}

var parentDirectory: String {
URL(fileURLWithPath: self.source).deletingLastPathComponent().path
}

var filename: String {
URL(fileURLWithPath: self.source).lastPathComponent
}

/// Create an isolated temporary directory containing only the target file via hardlink
func createIsolatedFileShare() throws -> String {
// Create deterministic temp directory
let combinedPath = "\(self.source)|\(self.destination)"
let sourceHash = try hashMountSource(source: combinedPath)
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("containerization-file-mount-\(sourceHash)")

// Use destination filename for the hardlink instead of source filename
let destinationFilename = URL(fileURLWithPath: self.destination).lastPathComponent
let isolatedFile = tempDir.appendingPathComponent(destinationFilename)

// Check if hard link already exists
if FileManager.default.fileExists(atPath: isolatedFile.path) {
// Hard link already exists - nothing to do
return tempDir.path
}

// Validate source file exists and is a regular file
try validateSourceFile()

// Atomically create directory
try createDirectory(at: tempDir)

let sourceFile = URL(fileURLWithPath: self.source)

// Create the hard link, handling race conditions
do {
try FileManager.default.linkItem(at: sourceFile, to: isolatedFile)
} catch CocoaError.fileWriteFileExists {
// Another thread created the hardlink - that's fine
} catch {
throw ContainerizationError(.internalError, message: "Failed to create hardlink: \(error.localizedDescription)")
}

// Final verification that the hardlinked file exists
guard FileManager.default.fileExists(atPath: isolatedFile.path) else {
throw ContainerizationError(.notFound, message: "Failed to create hardlink at: \(isolatedFile.path)")
}

return tempDir.path
}

/// Release reference to an isolated file share directory
/// No-op to avoid race conditions in parallel test execution
static func releaseIsolatedFileShare(source: String, destination: String) {
// No cleanup during tests to avoid race conditions
// OS will clean up temp directories on reboot
}

/// Validate that the source file exists, is readable, and is not a symlink
private func validateSourceFile() throws {

// Check if file exists
guard FileManager.default.fileExists(atPath: self.source) else {
throw ContainerizationError(.notFound, message: "Source file does not exist: \(self.source)")
}

// Get file attributes to check if it's a regular file
let attributes = try FileManager.default.attributesOfItem(atPath: self.source)
let fileType = attributes[.type] as? FileAttributeType

// Reject symlinks to prevent following links to unintended targets
guard fileType != .typeSymbolicLink else {
throw ContainerizationError(.invalidArgument, message: "Cannot mount symlink: \(self.source)")
}

// Ensure it's a regular file
guard fileType == .typeRegular else {
throw ContainerizationError(.invalidArgument, message: "Source must be a regular file: \(self.source)")
}

// Check if file is readable
guard FileManager.default.isReadableFile(atPath: self.source) else {
throw ContainerizationError(.invalidArgument, message: "Source file is not readable: \(self.source)")
}
}

/// Atomically create directory (to prevent TOCTOU race conditions)
private func createDirectory(at url: URL) throws {
do {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700])
// Register for cleanup
if url.path.contains("containerization-file-mount-") {
VZVirtualMachineInstance.registerTempDirectory(url.path)
}
} catch CocoaError.fileWriteFileExists {
// Directory already exists, verify it's actually a directory
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory),
isDirectory.boolValue
else {
throw ContainerizationError(.invalidArgument, message: "Path exists but is not a directory: \(url.path)")
}
// Directory exists and is valid, continue
} catch {
throw ContainerizationError(.internalError, message: "Failed to create directory \(url.path): \(error.localizedDescription)")
}
}

func configure(config: inout VZVirtualMachineConfiguration) throws {
switch self.runtimeOptions {
case .virtioblk(let options):
Expand All @@ -140,11 +254,18 @@ extension Mount {
config.storageDevices.append(attachment)
case .virtiofs(_):
guard FileManager.default.fileExists(atPath: self.source) else {
throw ContainerizationError(.notFound, message: "directory \(source) does not exist")
throw ContainerizationError(.notFound, message: "path \(source) does not exist")
}

let shareSource: String
if isFile {
shareSource = try createIsolatedFileShare()
} else {
shareSource = self.source
}

let name = try hashMountSource(source: self.source)
let urlSource = URL(fileURLWithPath: source)
let name = try hashMountSource(source: shareSource)
let urlSource = URL(fileURLWithPath: shareSource)

let device = VZVirtioFileSystemDeviceConfiguration(tag: name)
device.share = VZSingleDirectoryShare(
Expand Down
Loading