Files
wehub-resource-sync 680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
chore: import upstream snapshot with attribution
2026-07-13 12:25:30 +08:00

453 lines
16 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationError
import Foundation
#if os(macOS)
import Virtualization
#endif
#if os(Linux)
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#endif
#endif
/// A filesystem mount exposed to a container.
public struct Mount: Sendable {
/// The filesystem or mount type. This is the string
/// that will be used for the mount syscall itself.
public var type: String
/// The source path of the mount.
public var source: String
/// The destination path of the mount.
public var destination: String
/// Filesystem or mount specific options.
public var options: [String]
/// Runtime specific options. This can be used
/// as a way to discern what kind of device a vmm
/// should create for this specific mount (virtioblock
/// virtiofs etc.).
public let runtimeOptions: RuntimeOptions
/// A type representing a "hint" of what type
/// of mount this really is (block, directory, purely
/// guest mount) and a set of type specific options, if any.
public enum RuntimeOptions: Sendable {
case virtioblk([String])
case virtiofs([String])
case shared
case any([String])
}
public init(
type: String,
source: String,
destination: String,
options: [String],
runtimeOptions: RuntimeOptions
) {
self.type = type
self.source = source
self.destination = destination
self.options = options
self.runtimeOptions = runtimeOptions
}
/// Mount representing a virtio block device.
public static func block(
format: String,
source: String,
destination: String,
options: [String] = [],
runtimeOptions: [String] = []
) -> Self {
.init(
type: format,
source: source,
destination: destination,
options: options,
runtimeOptions: .virtioblk(runtimeOptions)
)
}
/// Mount representing a virtiofs share.
public static func share(
source: String,
destination: String,
options: [String] = [],
runtimeOptions: [String] = []
) -> Self {
.init(
type: "virtiofs",
source: source,
destination: destination,
options: options,
runtimeOptions: .virtiofs(runtimeOptions)
)
}
/// A generic mount.
public static func any(
type: String,
source: String,
destination: String,
options: [String] = [],
runtimeOptions: [String] = []
) -> Self {
.init(
type: type,
source: source,
destination: destination,
options: options,
runtimeOptions: .any(runtimeOptions)
)
}
/// A mount referencing a shared pod volume by name.
public static func sharedMount(
name: String,
destination: String,
options: [String] = []
) -> Self {
.init(
type: "none",
source: name,
destination: destination,
options: options,
runtimeOptions: .shared
)
}
/// Clone the Mount to the provided path.
///
/// On macOS this uses `clonefile` (via `FileManager.copyItem`) for a
/// copy-on-write copy when the underlying filesystem supports it. On
/// Linux it tries `ioctl(FICLONE)` first (CoW on btrfs / xfs / bcachefs)
/// and falls back to a `SEEK_DATA`/`SEEK_HOLE` sparse copy that copies
/// only data ranges. This matters for EXT4 images produced by
/// `EXT4+Formatter`, which sparse-allocate via `lseek + 1-byte write` —
/// a non-sparse copy would inflate a ~50 MB alpine rootfs into a
/// fully-allocated 2 GiB clone and exhaust the integration suite's
/// writable layer in ~30 tests.
public func clone(to: String) throws -> Self {
#if os(Linux)
try Self.linuxSparseCopy(from: self.source, to: to)
#else
try FileManager.default.copyItem(atPath: self.source, toPath: to)
#endif
return .init(
type: self.type,
source: to,
destination: self.destination,
options: self.options,
runtimeOptions: self.runtimeOptions
)
}
#if os(Linux)
/// Copy `src` to `dst`, preferring a CoW reflink (`ioctl(FICLONE)`) and
/// falling back to a SEEK_DATA/SEEK_HOLE sparse copy. The reflink path
/// succeeds on btrfs / xfs (`reflink=1`) / bcachefs; on ext4 / tmpfs /
/// overlayfs it fails fast with EOPNOTSUPP/EXDEV/EINVAL and we walk
/// the hole map instead. The sparse-copy path also handles
/// filesystems that don't support hole-seeking (the very first
/// SEEK_DATA returns EINVAL) by copying the remainder verbatim. Mode
/// bits are preserved from the source.
private static func linuxSparseCopy(from src: String, to dst: String) throws {
// Stable Linux ABI since 3.1 (ext4, tmpfs, overlayfs all support it).
// Re-declared here so the build doesn't depend on whether the
// Glibc/Musl Swift overlay re-exports them.
let SEEK_DATA: Int32 = 3
let SEEK_HOLE: Int32 = 4
// _IOW(0x94, 9, int) on every Linux arch we target (x86_64, aarch64).
let FICLONE: CUnsignedLong = 0x4004_9409
let srcFd = open(src, O_RDONLY | O_CLOEXEC)
guard srcFd >= 0 else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
defer { _ = close(srcFd) }
var st = stat()
guard fstat(srcFd, &st) == 0 else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
let size = off_t(st.st_size)
let mode = mode_t(st.st_mode & 0o7777)
let dstFd = open(dst, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, mode)
guard dstFd >= 0 else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
defer { _ = close(dstFd) }
// FICLONE atomically replaces dst's contents with a CoW clone of
// src — sets size and contents in one shot, no ftruncate needed
// afterwards. On failure FICLONE guarantees dst is untouched, so
// we can safely fall through to the sparse-copy path. ioctl(2) is
// variadic; type-pun via a fixed-arity function pointer (same
// pattern as ContainerizationOS.Socket).
let ioctlFICLONE: @convention(c) (CInt, CUnsignedLong, CInt) -> CInt = ioctl
if ioctlFICLONE(dstFd, FICLONE, srcFd) == 0 {
return
}
// Set the destination size up front so any trailing hole survives —
// we only ever pwrite data ranges, never zero-fill.
guard ftruncate(dstFd, size) == 0 else {
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
let bufSize = 1 << 20 // 1 MiB
let buf = UnsafeMutableRawPointer.allocate(byteCount: bufSize, alignment: 16)
defer { buf.deallocate() }
var pos: off_t = 0
while pos < size {
let dataStart = lseek(srcFd, pos, SEEK_DATA)
if dataStart < 0 {
// ENXIO: no more data — rest is hole, already covered by ftruncate.
if errno == ENXIO {
break
}
// EINVAL/ENOTSUP: filesystem doesn't support SEEK_DATA. Treat
// the remainder as one big data range and copy it verbatim.
try Self.copyRange(srcFd: srcFd, dstFd: dstFd, start: pos, end: size, buf: buf, bufSize: bufSize)
break
}
// SEEK_HOLE returns end-of-file when there's no trailing hole.
let dataEnd = lseek(srcFd, dataStart, SEEK_HOLE)
let endOff: off_t = dataEnd < 0 ? size : dataEnd
try Self.copyRange(srcFd: srcFd, dstFd: dstFd, start: dataStart, end: endOff, buf: buf, bufSize: bufSize)
pos = endOff
}
}
private static func copyRange(
srcFd: Int32,
dstFd: Int32,
start: off_t,
end: off_t,
buf: UnsafeMutableRawPointer,
bufSize: Int
) throws {
var off = start
while off < end {
let want = Int(min(off_t(bufSize), end - off))
let nread = pread(srcFd, buf, want, off)
if nread < 0 {
if errno == EINTR { continue }
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
if nread == 0 {
// Source shorter than fstat reported — shouldn't happen, but
// bail rather than spin.
return
}
var written = 0
while written < nread {
let nwrite = pwrite(dstFd, buf.advanced(by: written), nread - written, off + off_t(written))
if nwrite < 0 {
if errno == EINTR { continue }
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO)
}
written += nwrite
}
off += off_t(nread)
}
}
#endif
}
#if os(macOS)
extension Mount {
private enum StorageAttachmentType {
case diskImage
case networkBlockDevice
}
private var storageAttachmentType: StorageAttachmentType {
let nbdSchemes = ["nbd://", "nbds://", "nbd+unix://", "nbds+unix://"]
if nbdSchemes.contains(where: { self.source.hasPrefix($0) }) {
return .networkBlockDevice
}
return .diskImage
}
func configure(config: inout VZVirtualMachineConfiguration) throws {
switch self.runtimeOptions {
case .virtioblk(let options):
let device: VZStorageDeviceAttachment
switch self.storageAttachmentType {
case .networkBlockDevice:
device = try VZNetworkBlockDeviceStorageDeviceAttachment.mountToVZAttachment(mount: self, options: options)
case .diskImage:
device = try VZDiskImageStorageDeviceAttachment.mountToVZAttachment(mount: self, options: options)
}
let attachment = VZVirtioBlockDeviceConfiguration(attachment: device)
config.storageDevices.append(attachment)
case .virtiofs(_):
// VirtioFS mounts are handled centrally via VZMultipleDirectoryShare in VZVirtualMachineInstance
// No per-mount device configuration needed
break
case .shared, .any:
break
}
}
}
extension VZDiskImageStorageDeviceAttachment {
static func mountToVZAttachment(mount: Mount, options: [String]) throws -> VZDiskImageStorageDeviceAttachment {
var synchronizationMode: VZDiskImageSynchronizationMode = .fsync
var cachingMode: VZDiskImageCachingMode = .cached
for option in options {
let split = option.split(separator: "=")
if split.count != 2 {
continue
}
let key = String(split[0])
let value = String(split[1])
switch key {
case "vzDiskImageCachingMode":
switch value {
case "automatic":
cachingMode = .automatic
case "cached":
cachingMode = .cached
case "uncached":
cachingMode = .uncached
default:
throw ContainerizationError(
.invalidArgument,
message: "unknown vzDiskImageCachingMode value for virtio block device: \(value)"
)
}
case "vzDiskImageSynchronizationMode":
switch value {
case "full":
synchronizationMode = .full
case "fsync":
synchronizationMode = .fsync
case "none":
synchronizationMode = .none
default:
throw ContainerizationError(
.invalidArgument,
message: "unknown vzDiskImageSynchronizationMode value for virtio block device: \(value)"
)
}
default:
throw ContainerizationError(
.invalidArgument,
message: "unknown vmm option encountered: \(key)"
)
}
}
return try VZDiskImageStorageDeviceAttachment(
url: URL(filePath: mount.source),
readOnly: mount.readonly,
cachingMode: cachingMode,
synchronizationMode: synchronizationMode
)
}
}
extension VZNetworkBlockDeviceStorageDeviceAttachment {
static func mountToVZAttachment(mount: Mount, options: [String]) throws -> VZNetworkBlockDeviceStorageDeviceAttachment {
guard let url = URL(string: mount.source) else {
throw ContainerizationError(
.invalidArgument,
message: "invalid NBD URL: \(mount.source)"
)
}
var timeout: TimeInterval = 5
var synchronizationMode: VZDiskSynchronizationMode = .full
for option in options {
let split = option.split(separator: "=")
if split.count != 2 {
continue
}
let key = String(split[0])
let value = String(split[1])
switch key {
case "vzTimeout":
guard let t = TimeInterval(value) else {
throw ContainerizationError(
.invalidArgument,
message: "invalid vzTimeout value for NBD device: \(value)"
)
}
timeout = t
case "vzSynchronizationMode":
switch value {
case "full":
synchronizationMode = .full
case "none":
synchronizationMode = .none
default:
throw ContainerizationError(
.invalidArgument,
message: "unknown vzSynchronizationMode value for NBD device: \(value)"
)
}
default:
throw ContainerizationError(
.invalidArgument,
message: "unknown vmm option encountered: \(key)"
)
}
}
return try VZNetworkBlockDeviceStorageDeviceAttachment(
url: url,
timeout: timeout,
isForcedReadOnly: mount.readonly,
synchronizationMode: synchronizationMode
)
}
}
#endif
extension Mount {
fileprivate var readonly: Bool {
self.options.contains("ro")
}
/// Returns true if this mount is a virtio block device.
public var isBlock: Bool {
if case .virtioblk = self.runtimeOptions {
return true
}
return false
}
}