From f84b79b52264913011b7bdb3ec30dd47fff54d50 Mon Sep 17 00:00:00 2001 From: Jaewon Date: Wed, 16 Sep 2026 15:10:37 -0700 Subject: [PATCH 1/7] Add filesystem stats in ContainerStatistics * Add `filesystem (usedBytes, inodesUsed)` field to protobuf `ContainerStats` * Add `ManagedContainer.filesystemStats` which stats the file system using `statfs` syscall. `vminitd` in guest uses this to get the measurement on `containerStatistics` calls * Update `ContainerStatistics` to include `filesystem` fields. --- .../ContainerStatistics.swift | 20 ++++- .../SandboxContext/SandboxContext.pb.swift | 76 ++++++++++++++++++- .../SandboxContext/SandboxContext.proto | 10 +++ Sources/Containerization/Vminitd.swift | 8 ++ Sources/Integration/ContainerTests.swift | 9 +++ .../VminitdCore/ManagedContainer.swift | 19 +++++ vminitd/Sources/VminitdCore/Server+GRPC.swift | 22 +++++- 7 files changed, 158 insertions(+), 6 deletions(-) diff --git a/Sources/Containerization/ContainerStatistics.swift b/Sources/Containerization/ContainerStatistics.swift index c41678713..30b7038af 100644 --- a/Sources/Containerization/ContainerStatistics.swift +++ b/Sources/Containerization/ContainerStatistics.swift @@ -23,6 +23,7 @@ public struct ContainerStatistics: Sendable { public var blockIO: BlockIOStatistics? public var networks: [NetworkStatistics]? public var memoryEvents: MemoryEventStatistics? + public var filesystem: FilesystemStatistics? public init( id: String, @@ -31,7 +32,8 @@ public struct ContainerStatistics: Sendable { cpu: CPUStatistics? = nil, blockIO: BlockIOStatistics? = nil, networks: [NetworkStatistics]? = nil, - memoryEvents: MemoryEventStatistics? = nil + memoryEvents: MemoryEventStatistics? = nil, + filesystem: FilesystemStatistics? = nil ) { self.id = id self.process = process @@ -40,6 +42,7 @@ public struct ContainerStatistics: Sendable { self.blockIO = blockIO self.networks = networks self.memoryEvents = memoryEvents + self.filesystem = filesystem } /// Process statistics for a container. @@ -220,6 +223,17 @@ public struct ContainerStatistics: Sendable { self.oomKill = oomKill } } + + /// Filesystem occupancy for a container's writable rootfs mount. + public struct FilesystemStatistics: Sendable { + public var usedBytes: UInt64 + public var inodesUsed: UInt64 + + public init(usedBytes: UInt64, inodesUsed: UInt64) { + self.usedBytes = usedBytes + self.inodesUsed = inodesUsed + } + } } /// Categories of statistics that can be requested. @@ -242,7 +256,9 @@ public struct StatCategory: OptionSet, Sendable { public static let network = StatCategory(rawValue: 1 << 4) /// Memory event counters (OOM kills, pressure events, etc.). public static let memoryEvents = StatCategory(rawValue: 1 << 5) + /// Filesystem occupancy statistics. + public static let filesystem = StatCategory(rawValue: 1 << 6) /// All available statistics categories. - public static let all: StatCategory = [.process, .memory, .cpu, .blockIO, .network, .memoryEvents] + public static let all: StatCategory = [.process, .memory, .cpu, .blockIO, .network, .memoryEvents, .filesystem] } diff --git a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift index 48dd535ae..1da22d621 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift +++ b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift @@ -51,6 +51,7 @@ public nonisolated enum Com_Apple_Containerization_Sandbox_V3_StatCategory: Swif case blockIo // = 4 case network // = 5 case memoryEvents // = 6 + case filesystem // = 7 case UNRECOGNIZED(Int) public init() { @@ -66,6 +67,7 @@ public nonisolated enum Com_Apple_Containerization_Sandbox_V3_StatCategory: Swif case 4: self = .blockIo case 5: self = .network case 6: self = .memoryEvents + case 7: self = .filesystem default: self = .UNRECOGNIZED(rawValue) } } @@ -79,6 +81,7 @@ public nonisolated enum Com_Apple_Containerization_Sandbox_V3_StatCategory: Swif case .blockIo: return 4 case .network: return 5 case .memoryEvents: return 6 + case .filesystem: return 7 case .UNRECOGNIZED(let i): return i } } @@ -92,6 +95,7 @@ public nonisolated enum Com_Apple_Containerization_Sandbox_V3_StatCategory: Swif .blockIo, .network, .memoryEvents, + .filesystem, ] } @@ -1595,6 +1599,15 @@ public nonisolated struct Com_Apple_Containerization_Sandbox_V3_ContainerStats: /// Clears the value of `memoryEvents`. Subsequent reads from it will return its default value. public mutating func clearMemoryEvents() {_uniqueStorage()._memoryEvents = nil} + public var filesystem: Com_Apple_Containerization_Sandbox_V3_FilesystemStats { + get {_storage._filesystem ?? Com_Apple_Containerization_Sandbox_V3_FilesystemStats()} + set {_uniqueStorage()._filesystem = newValue} + } + /// Returns true if `filesystem` has been explicitly set. + public var hasFilesystem: Bool {_storage._filesystem != nil} + /// Clears the value of `filesystem`. Subsequent reads from it will return its default value. + public mutating func clearFilesystem() {_uniqueStorage()._filesystem = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1768,12 +1781,29 @@ public nonisolated struct Com_Apple_Containerization_Sandbox_V3_MemoryEventStats public init() {} } +/// Filesystem occupancy for a container's writable rootfs mount, from statfs(2). +public nonisolated struct Com_Apple_Containerization_Sandbox_V3_FilesystemStats: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Bytes currently used on the filesystem (f_blocks - f_bfree) * f_frsize. + public var usedBytes: UInt64 = 0 + + /// Inodes currently used on the filesystem (f_files - f_ffree). + public var inodesUsed: UInt64 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate nonisolated let _protobuf_package = "com.apple.containerization.sandbox.v3" nonisolated extension Com_Apple_Containerization_Sandbox_V3_StatCategory: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0STAT_CATEGORY_UNSPECIFIED\0\u{1}STAT_CATEGORY_PROCESS\0\u{1}STAT_CATEGORY_MEMORY\0\u{1}STAT_CATEGORY_CPU\0\u{1}STAT_CATEGORY_BLOCK_IO\0\u{1}STAT_CATEGORY_NETWORK\0\u{1}STAT_CATEGORY_MEMORY_EVENTS\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0STAT_CATEGORY_UNSPECIFIED\0\u{1}STAT_CATEGORY_PROCESS\0\u{1}STAT_CATEGORY_MEMORY\0\u{1}STAT_CATEGORY_CPU\0\u{1}STAT_CATEGORY_BLOCK_IO\0\u{1}STAT_CATEGORY_NETWORK\0\u{1}STAT_CATEGORY_MEMORY_EVENTS\0\u{1}STAT_CATEGORY_FILESYSTEM\0") } nonisolated extension Com_Apple_Containerization_Sandbox_V3_Stdio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { @@ -4232,7 +4262,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStatisticsR nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ContainerStats" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}container_id\0\u{1}process\0\u{1}memory\0\u{1}cpu\0\u{3}block_io\0\u{1}networks\0\u{3}memory_events\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}container_id\0\u{1}process\0\u{1}memory\0\u{1}cpu\0\u{3}block_io\0\u{1}networks\0\u{3}memory_events\0\u{1}filesystem\0") fileprivate class _StorageClass { var _containerID: String = String() @@ -4242,6 +4272,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: Swif var _blockIo: Com_Apple_Containerization_Sandbox_V3_BlockIOStats? = nil var _networks: [Com_Apple_Containerization_Sandbox_V3_NetworkStats] = [] var _memoryEvents: Com_Apple_Containerization_Sandbox_V3_MemoryEventStats? = nil + var _filesystem: Com_Apple_Containerization_Sandbox_V3_FilesystemStats? = nil // This property is used as the initial default value for new instances of the type. // The type itself is protecting the reference to its storage via CoW semantics. @@ -4259,6 +4290,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: Swif _blockIo = source._blockIo _networks = source._networks _memoryEvents = source._memoryEvents + _filesystem = source._filesystem } } @@ -4284,6 +4316,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: Swif case 5: try { try decoder.decodeSingularMessageField(value: &_storage._blockIo) }() case 6: try { try decoder.decodeRepeatedMessageField(value: &_storage._networks) }() case 7: try { try decoder.decodeSingularMessageField(value: &_storage._memoryEvents) }() + case 8: try { try decoder.decodeSingularMessageField(value: &_storage._filesystem) }() default: break } } @@ -4317,6 +4350,9 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: Swif try { if let v = _storage._memoryEvents { try visitor.visitSingularMessageField(value: v, fieldNumber: 7) } }() + try { if let v = _storage._filesystem { + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + } }() } try unknownFields.traverse(visitor: &visitor) } @@ -4333,6 +4369,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: Swif if _storage._blockIo != rhs_storage._blockIo {return false} if _storage._networks != rhs_storage._networks {return false} if _storage._memoryEvents != rhs_storage._memoryEvents {return false} + if _storage._filesystem != rhs_storage._filesystem {return false} return true } if !storagesAreEqual {return false} @@ -4736,3 +4773,38 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_MemoryEventStats: Sw return true } } + +nonisolated extension Com_Apple_Containerization_Sandbox_V3_FilesystemStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".FilesystemStats" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}used_bytes\0\u{3}inodes_used\0") + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.usedBytes) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.inodesUsed) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.usedBytes != 0 { + try visitor.visitSingularUInt64Field(value: self.usedBytes, fieldNumber: 1) + } + if self.inodesUsed != 0 { + try visitor.visitSingularUInt64Field(value: self.inodesUsed, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_FilesystemStats, rhs: Com_Apple_Containerization_Sandbox_V3_FilesystemStats) -> Bool { + if lhs.usedBytes != rhs.usedBytes {return false} + if lhs.inodesUsed != rhs.inodesUsed {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/Containerization/SandboxContext/SandboxContext.proto b/Sources/Containerization/SandboxContext/SandboxContext.proto index 8cfc0877b..dd134bdaa 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.proto +++ b/Sources/Containerization/SandboxContext/SandboxContext.proto @@ -408,6 +408,7 @@ enum StatCategory { STAT_CATEGORY_BLOCK_IO = 4; STAT_CATEGORY_NETWORK = 5; STAT_CATEGORY_MEMORY_EVENTS = 6; + STAT_CATEGORY_FILESYSTEM = 7; } message ContainerStatisticsRequest { @@ -427,6 +428,7 @@ message ContainerStats { BlockIOStats block_io = 5; repeated NetworkStats networks = 6; MemoryEventStats memory_events = 7; + FilesystemStats filesystem = 8; } message ProcessStats { @@ -500,3 +502,11 @@ message MemoryEventStats { // Number of times charge for memory failed because of limit. uint64 oom_group_kill = 6; } + +// Filesystem occupancy for a container's writable rootfs mount, from statfs(2). +message FilesystemStats { + // Bytes currently used on the filesystem (f_blocks - f_bfree) * f_frsize. + uint64 used_bytes = 1; + // Inodes currently used on the filesystem (f_files - f_ffree). + uint64 inodes_used = 2; +} diff --git a/Sources/Containerization/Vminitd.swift b/Sources/Containerization/Vminitd.swift index 68d3b5bef..8e01c2fd5 100644 --- a/Sources/Containerization/Vminitd.swift +++ b/Sources/Containerization/Vminitd.swift @@ -177,6 +177,11 @@ extension Vminitd: VirtualMachineAgent { max: protoStats.memoryEvents.max, oom: protoStats.memoryEvents.oom, oomKill: protoStats.memoryEvents.oomKill + ) : nil, + filesystem: categories.contains(.filesystem) && protoStats.hasFilesystem + ? .init( + usedBytes: protoStats.filesystem.usedBytes, + inodesUsed: protoStats.filesystem.inodesUsed ) : nil ) } @@ -632,6 +637,9 @@ extension StatCategory { if contains(.memoryEvents) { categories.append(.memoryEvents) } + if contains(.filesystem) { + categories.append(.filesystem) + } return categories } } diff --git a/Sources/Integration/ContainerTests.swift b/Sources/Integration/ContainerTests.swift index 746db856b..83fe9ef3c 100644 --- a/Sources/Integration/ContainerTests.swift +++ b/Sources/Integration/ContainerTests.swift @@ -884,11 +884,20 @@ extension IntegrationSuite { throw IntegrationError.assert(msg: "CPU usage should be > 0, got \(stats.cpu?.usageUsec ?? 0)") } + guard let filesystem = stats.filesystem, filesystem.usedBytes > 0 else { + throw IntegrationError.assert(msg: "filesystem usedBytes should be > 0, got \(stats.filesystem?.usedBytes ?? 0)") + } + + guard filesystem.inodesUsed > 0 else { + throw IntegrationError.assert(msg: "filesystem inodesUsed should be > 0, got \(filesystem.inodesUsed)") + } + print("Container statistics:") print(" Processes: \(process.current)") print(" Memory: \(memory.usageBytes) bytes") print(" CPU: \(cpu.usageUsec) usec") print(" Networks: \(stats.networks?.count ?? 0) interfaces") + print(" Filesystem: \(filesystem.usedBytes) bytes, \(filesystem.inodesUsed) inodes") try await container.stop() } catch { diff --git a/vminitd/Sources/VminitdCore/ManagedContainer.swift b/vminitd/Sources/VminitdCore/ManagedContainer.swift index 43645ce52..a8585b406 100644 --- a/vminitd/Sources/VminitdCore/ManagedContainer.swift +++ b/vminitd/Sources/VminitdCore/ManagedContainer.swift @@ -16,6 +16,12 @@ #if os(Linux) +#if canImport(Musl) +import Musl +#elseif canImport(Glibc) +import Glibc +#endif + import Cgroup import ContainerizationError import ContainerizationOCI @@ -239,6 +245,19 @@ extension ManagedContainer { try self.cgroupManager.getMemoryEvents() } + func filesystemStats() throws -> (usedBytes: UInt64, inodesUsed: UInt64) { + var s = statfs() + guard statfs(self.bundle.rootfsPath.path, &s) == 0 else { + throw ContainerizationError( + .internalError, + message: "statfs(\(self.bundle.rootfsPath.path)) failed: errno \(errno)" + ) + } + let usedBytes = (UInt64(s.f_blocks) - UInt64(s.f_bfree)) * UInt64(s.f_bsize) + let inodesUsed = UInt64(s.f_files) - UInt64(s.f_ffree) + return (usedBytes, inodesUsed) + } + func getExecOrInit(execID: String) throws -> any ContainerProcess { if execID == self.id { return self.initProcess diff --git a/vminitd/Sources/VminitdCore/Server+GRPC.swift b/vminitd/Sources/VminitdCore/Server+GRPC.swift index cca09db72..0ab49c730 100644 --- a/vminitd/Sources/VminitdCore/Server+GRPC.swift +++ b/vminitd/Sources/VminitdCore/Server+GRPC.swift @@ -1669,6 +1669,7 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ let wantBlockIO = wantAll || categories.contains(.blockIo) let wantNetwork = wantAll || categories.contains(.network) let wantMemoryEvents = wantAll || categories.contains(.memoryEvents) + let wantFilesystem = wantAll || categories.contains(.filesystem) // Get all network interfaces (skip loopback) only if needed let interfaces = wantNetwork ? try getNetworkInterfaces() : [] @@ -1723,18 +1724,26 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ memoryEvents = try await container.getMemoryEvents() } + // Get filesystem usage only if requested + var filesystemStats: (usedBytes: UInt64, inodesUsed: UInt64)? + if wantFilesystem { + filesystemStats = try await container.filesystemStats() + } + containerStats.append( mapStatsToProto( containerID: containerID, cgStats: cgStats, networkStats: networkStats, memoryEvents: memoryEvents, + filesystemStats: filesystemStats, wantProcess: wantProcess, wantMemory: wantMemory, wantCPU: wantCPU, wantBlockIO: wantBlockIO, wantNetwork: wantNetwork, - wantMemoryEvents: wantMemoryEvents + wantMemoryEvents: wantMemoryEvents, + wantFilesystem: wantFilesystem ) ) } @@ -1800,12 +1809,14 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ cgStats: Cgroup2Stats?, networkStats: [Com_Apple_Containerization_Sandbox_V3_NetworkStats], memoryEvents: MemoryEvents?, + filesystemStats: (usedBytes: UInt64, inodesUsed: UInt64)?, wantProcess: Bool, wantMemory: Bool, wantCPU: Bool, wantBlockIO: Bool, wantNetwork: Bool, - wantMemoryEvents: Bool + wantMemoryEvents: Bool, + wantFilesystem: Bool ) -> Com_Apple_Containerization_Sandbox_V3_ContainerStats { .with { $0.containerID = containerID @@ -1877,6 +1888,13 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ $0.oomKill = events.oomKill } } + + if wantFilesystem, let fs = filesystemStats { + $0.filesystem = .with { + $0.usedBytes = fs.usedBytes + $0.inodesUsed = fs.inodesUsed + } + } } } From f13564fca7f49c84f7738462a8118b1d93b0d484 Mon Sep 17 00:00:00 2001 From: Jaewon Date: Wed, 16 Sep 2026 17:00:55 -0700 Subject: [PATCH 2/7] Make statfs shim for linux `statfs` isn't reliably importable from Swift via the Musl/Glibc system modulemap across architectures. --- vminitd/Sources/LCShim/include/syscall.h | 14 ++++++++++++++ vminitd/Sources/LCShim/syscall.c | 13 +++++++++++++ .../Sources/VminitdCore/ManagedContainer.swift | 15 +++++---------- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/vminitd/Sources/LCShim/include/syscall.h b/vminitd/Sources/LCShim/include/syscall.h index 0cabfbcfe..a048a16ce 100644 --- a/vminitd/Sources/LCShim/include/syscall.h +++ b/vminitd/Sources/LCShim/include/syscall.h @@ -104,4 +104,18 @@ int CZ_pidfd_getfd(int pidfd, int targetfd, unsigned int flags); int CZ_prctl_set_no_new_privs(); +// Mirrors the fields of struct statfs needed to compute filesystem usage. +// struct statfs itself isn't reliably importable from Swift across all +// target architectures via the Musl/Glibc modulemaps, so this wraps +// statfs(2) and copies out only the plain-integer fields callers need. +typedef struct { + unsigned long long f_blocks; + unsigned long long f_bfree; + unsigned long long f_bsize; + unsigned long long f_files; + unsigned long long f_ffree; +} CZ_Statfs; + +int CZ_statfs(const char *path, CZ_Statfs *out); + #endif diff --git a/vminitd/Sources/LCShim/syscall.c b/vminitd/Sources/LCShim/syscall.c index 094f6c61b..a0ca820f3 100644 --- a/vminitd/Sources/LCShim/syscall.c +++ b/vminitd/Sources/LCShim/syscall.c @@ -49,4 +49,17 @@ int CZ_setrlimit(int resource, unsigned long long soft, limit.rlim_max = (rlim_t)hard; return setrlimit(resource, &limit); } + +int CZ_statfs(const char *path, CZ_Statfs *out) { + struct statfs s; + if (statfs(path, &s) != 0) { + return -1; + } + out->f_blocks = (unsigned long long)s.f_blocks; + out->f_bfree = (unsigned long long)s.f_bfree; + out->f_bsize = (unsigned long long)s.f_bsize; + out->f_files = (unsigned long long)s.f_files; + out->f_ffree = (unsigned long long)s.f_ffree; + return 0; +} #endif diff --git a/vminitd/Sources/VminitdCore/ManagedContainer.swift b/vminitd/Sources/VminitdCore/ManagedContainer.swift index a8585b406..6efa900bd 100644 --- a/vminitd/Sources/VminitdCore/ManagedContainer.swift +++ b/vminitd/Sources/VminitdCore/ManagedContainer.swift @@ -16,17 +16,12 @@ #if os(Linux) -#if canImport(Musl) -import Musl -#elseif canImport(Glibc) -import Glibc -#endif - import Cgroup import ContainerizationError import ContainerizationOCI import ContainerizationOS import Foundation +import LCShim import Logging public actor ManagedContainer { @@ -246,15 +241,15 @@ extension ManagedContainer { } func filesystemStats() throws -> (usedBytes: UInt64, inodesUsed: UInt64) { - var s = statfs() - guard statfs(self.bundle.rootfsPath.path, &s) == 0 else { + var s = CZ_Statfs() + guard CZ_statfs(self.bundle.rootfsPath.path, &s) == 0 else { throw ContainerizationError( .internalError, message: "statfs(\(self.bundle.rootfsPath.path)) failed: errno \(errno)" ) } - let usedBytes = (UInt64(s.f_blocks) - UInt64(s.f_bfree)) * UInt64(s.f_bsize) - let inodesUsed = UInt64(s.f_files) - UInt64(s.f_ffree) + let usedBytes = (s.f_blocks - s.f_bfree) * s.f_bsize + let inodesUsed = s.f_files - s.f_ffree return (usedBytes, inodesUsed) } From ce46fd81964d5ea183ba6782f0a201b0ecf7503f Mon Sep 17 00:00:00 2001 From: Jaewon Date: Mon, 21 Sep 2026 10:26:26 -0700 Subject: [PATCH 3/7] [wip] Expose every statfs field in CZ_Statfs * It should not do anything clever --- vminitd/Sources/LCShim/include/syscall.h | 17 ++++++++++++----- vminitd/Sources/LCShim/syscall.c | 9 ++++++++- .../Sources/VminitdCore/ManagedContainer.swift | 6 ++---- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/vminitd/Sources/LCShim/include/syscall.h b/vminitd/Sources/LCShim/include/syscall.h index a048a16ce..62ae79fb9 100644 --- a/vminitd/Sources/LCShim/include/syscall.h +++ b/vminitd/Sources/LCShim/include/syscall.h @@ -104,16 +104,23 @@ int CZ_pidfd_getfd(int pidfd, int targetfd, unsigned int flags); int CZ_prctl_set_no_new_privs(); -// Mirrors the fields of struct statfs needed to compute filesystem usage. -// struct statfs itself isn't reliably importable from Swift across all -// target architectures via the Musl/Glibc modulemaps, so this wraps -// statfs(2) and copies out only the plain-integer fields callers need. +// Mirrors struct statfs (minus its reserved padding). struct statfs itself +// isn't reliably importable from Swift across all target architectures via +// the Musl/Glibc modulemaps, so this wraps statfs(2) and copies out plain +// integer fields instead. f_fsid (a 2-int opaque identifier) is packed into +// a single 64-bit value. typedef struct { + long long f_type; + unsigned long long f_bsize; unsigned long long f_blocks; unsigned long long f_bfree; - unsigned long long f_bsize; + unsigned long long f_bavail; unsigned long long f_files; unsigned long long f_ffree; + long long f_fsid; + unsigned long long f_namelen; + unsigned long long f_frsize; + unsigned long long f_flags; } CZ_Statfs; int CZ_statfs(const char *path, CZ_Statfs *out); diff --git a/vminitd/Sources/LCShim/syscall.c b/vminitd/Sources/LCShim/syscall.c index a0ca820f3..e7a4870d9 100644 --- a/vminitd/Sources/LCShim/syscall.c +++ b/vminitd/Sources/LCShim/syscall.c @@ -55,11 +55,18 @@ int CZ_statfs(const char *path, CZ_Statfs *out) { if (statfs(path, &s) != 0) { return -1; } + out->f_type = (long long)s.f_type; + out->f_bsize = (unsigned long long)s.f_bsize; out->f_blocks = (unsigned long long)s.f_blocks; out->f_bfree = (unsigned long long)s.f_bfree; - out->f_bsize = (unsigned long long)s.f_bsize; + out->f_bavail = (unsigned long long)s.f_bavail; out->f_files = (unsigned long long)s.f_files; out->f_ffree = (unsigned long long)s.f_ffree; + out->f_fsid = ((long long)s.f_fsid.__val[0] << 32) | + (unsigned int)s.f_fsid.__val[1]; + out->f_namelen = (unsigned long long)s.f_namelen; + out->f_frsize = (unsigned long long)s.f_frsize; + out->f_flags = (unsigned long long)s.f_flags; return 0; } #endif diff --git a/vminitd/Sources/VminitdCore/ManagedContainer.swift b/vminitd/Sources/VminitdCore/ManagedContainer.swift index 6efa900bd..776e279b6 100644 --- a/vminitd/Sources/VminitdCore/ManagedContainer.swift +++ b/vminitd/Sources/VminitdCore/ManagedContainer.swift @@ -240,7 +240,7 @@ extension ManagedContainer { try self.cgroupManager.getMemoryEvents() } - func filesystemStats() throws -> (usedBytes: UInt64, inodesUsed: UInt64) { + func filesystemStats() throws -> CZ_Statfs { var s = CZ_Statfs() guard CZ_statfs(self.bundle.rootfsPath.path, &s) == 0 else { throw ContainerizationError( @@ -248,9 +248,7 @@ extension ManagedContainer { message: "statfs(\(self.bundle.rootfsPath.path)) failed: errno \(errno)" ) } - let usedBytes = (s.f_blocks - s.f_bfree) * s.f_bsize - let inodesUsed = s.f_files - s.f_ffree - return (usedBytes, inodesUsed) + return s } func getExecOrInit(execID: String) throws -> any ContainerProcess { From b9b636530374fd660f56f4d4c5a56881c2183d07 Mon Sep 17 00:00:00 2001 From: Jaewon Date: Mon, 21 Sep 2026 10:27:54 -0700 Subject: [PATCH 4/7] [wip] Extend filesystemStats proto fields --- .../SandboxContext/SandboxContext.pb.swift | 53 ++++++++++++++----- .../SandboxContext/SandboxContext.proto | 17 ++++-- vminitd/Sources/VminitdCore/Server+GRPC.swift | 11 ++-- 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift index 1da22d621..a745b1c39 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift +++ b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift @@ -1781,17 +1781,27 @@ public nonisolated struct Com_Apple_Containerization_Sandbox_V3_MemoryEventStats public init() {} } -/// Filesystem occupancy for a container's writable rootfs mount, from statfs(2). +/// Raw statfs(2) fields for a container's writable rootfs mount, sufficient +/// to compute used bytes/inodes host-side. public nonisolated struct Com_Apple_Containerization_Sandbox_V3_FilesystemStats: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - /// Bytes currently used on the filesystem (f_blocks - f_bfree) * f_frsize. - public var usedBytes: UInt64 = 0 + /// f_bsize: block size in bytes; the unit blocks/free_blocks are counted in. + public var blockSize: UInt64 = 0 - /// Inodes currently used on the filesystem (f_files - f_ffree). - public var inodesUsed: UInt64 = 0 + /// f_blocks: total blocks in the filesystem. + public var blocks: UInt64 = 0 + + /// f_bfree: free blocks in the filesystem (includes blocks reserved for root). + public var freeBlocks: UInt64 = 0 + + /// f_files: total inodes in the filesystem. + public var inodes: UInt64 = 0 + + /// f_ffree: free inodes in the filesystem. + public var freeInodes: UInt64 = 0 public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -4776,7 +4786,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_MemoryEventStats: Sw nonisolated extension Com_Apple_Containerization_Sandbox_V3_FilesystemStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".FilesystemStats" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}used_bytes\0\u{3}inodes_used\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}block_size\0\u{1}blocks\0\u{3}free_blocks\0\u{1}inodes\0\u{3}free_inodes\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -4784,26 +4794,41 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_FilesystemStats: Swi // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularUInt64Field(value: &self.usedBytes) }() - case 2: try { try decoder.decodeSingularUInt64Field(value: &self.inodesUsed) }() + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.blockSize) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.blocks) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self.freeBlocks) }() + case 4: try { try decoder.decodeSingularUInt64Field(value: &self.inodes) }() + case 5: try { try decoder.decodeSingularUInt64Field(value: &self.freeInodes) }() default: break } } } public func traverse(visitor: inout V) throws { - if self.usedBytes != 0 { - try visitor.visitSingularUInt64Field(value: self.usedBytes, fieldNumber: 1) + if self.blockSize != 0 { + try visitor.visitSingularUInt64Field(value: self.blockSize, fieldNumber: 1) + } + if self.blocks != 0 { + try visitor.visitSingularUInt64Field(value: self.blocks, fieldNumber: 2) + } + if self.freeBlocks != 0 { + try visitor.visitSingularUInt64Field(value: self.freeBlocks, fieldNumber: 3) } - if self.inodesUsed != 0 { - try visitor.visitSingularUInt64Field(value: self.inodesUsed, fieldNumber: 2) + if self.inodes != 0 { + try visitor.visitSingularUInt64Field(value: self.inodes, fieldNumber: 4) + } + if self.freeInodes != 0 { + try visitor.visitSingularUInt64Field(value: self.freeInodes, fieldNumber: 5) } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_FilesystemStats, rhs: Com_Apple_Containerization_Sandbox_V3_FilesystemStats) -> Bool { - if lhs.usedBytes != rhs.usedBytes {return false} - if lhs.inodesUsed != rhs.inodesUsed {return false} + if lhs.blockSize != rhs.blockSize {return false} + if lhs.blocks != rhs.blocks {return false} + if lhs.freeBlocks != rhs.freeBlocks {return false} + if lhs.inodes != rhs.inodes {return false} + if lhs.freeInodes != rhs.freeInodes {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Sources/Containerization/SandboxContext/SandboxContext.proto b/Sources/Containerization/SandboxContext/SandboxContext.proto index dd134bdaa..15889203a 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.proto +++ b/Sources/Containerization/SandboxContext/SandboxContext.proto @@ -503,10 +503,17 @@ message MemoryEventStats { uint64 oom_group_kill = 6; } -// Filesystem occupancy for a container's writable rootfs mount, from statfs(2). +// Raw statfs(2) fields for a container's writable rootfs mount, sufficient +// to compute used bytes/inodes host-side. message FilesystemStats { - // Bytes currently used on the filesystem (f_blocks - f_bfree) * f_frsize. - uint64 used_bytes = 1; - // Inodes currently used on the filesystem (f_files - f_ffree). - uint64 inodes_used = 2; + // f_bsize: block size in bytes; the unit blocks/free_blocks are counted in. + uint64 block_size = 1; + // f_blocks: total blocks in the filesystem. + uint64 blocks = 2; + // f_bfree: free blocks in the filesystem (includes blocks reserved for root). + uint64 free_blocks = 3; + // f_files: total inodes in the filesystem. + uint64 inodes = 4; + // f_ffree: free inodes in the filesystem. + uint64 free_inodes = 5; } diff --git a/vminitd/Sources/VminitdCore/Server+GRPC.swift b/vminitd/Sources/VminitdCore/Server+GRPC.swift index 0ab49c730..6bd2c19b4 100644 --- a/vminitd/Sources/VminitdCore/Server+GRPC.swift +++ b/vminitd/Sources/VminitdCore/Server+GRPC.swift @@ -1725,7 +1725,7 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ } // Get filesystem usage only if requested - var filesystemStats: (usedBytes: UInt64, inodesUsed: UInt64)? + var filesystemStats: CZ_Statfs? if wantFilesystem { filesystemStats = try await container.filesystemStats() } @@ -1809,7 +1809,7 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ cgStats: Cgroup2Stats?, networkStats: [Com_Apple_Containerization_Sandbox_V3_NetworkStats], memoryEvents: MemoryEvents?, - filesystemStats: (usedBytes: UInt64, inodesUsed: UInt64)?, + filesystemStats: CZ_Statfs?, wantProcess: Bool, wantMemory: Bool, wantCPU: Bool, @@ -1891,8 +1891,11 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ if wantFilesystem, let fs = filesystemStats { $0.filesystem = .with { - $0.usedBytes = fs.usedBytes - $0.inodesUsed = fs.inodesUsed + $0.blockSize = fs.f_bsize + $0.blocks = fs.f_blocks + $0.freeBlocks = fs.f_bfree + $0.inodes = fs.f_files + $0.freeInodes = fs.f_ffree } } } From f4fa1ac736060f9bdcefd3e44f70706508ceee0f Mon Sep 17 00:00:00 2001 From: Jaewon Date: Mon, 21 Sep 2026 10:29:14 -0700 Subject: [PATCH 5/7] Update FilesystemStatistics * Expose blockSize, blocks, freeBlocks, inodes, freeInodes * usedBytes and inodesUsed can be computed using those fields. --- .../ContainerStatistics.swift | 22 ++++++++++++++----- Sources/Containerization/Vminitd.swift | 7 ++++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Sources/Containerization/ContainerStatistics.swift b/Sources/Containerization/ContainerStatistics.swift index 30b7038af..0cc3648c4 100644 --- a/Sources/Containerization/ContainerStatistics.swift +++ b/Sources/Containerization/ContainerStatistics.swift @@ -225,13 +225,25 @@ public struct ContainerStatistics: Sendable { } /// Filesystem occupancy for a container's writable rootfs mount. + /// Filesystem occupancy for a container's writable rootfs mount, from statfs(2). public struct FilesystemStatistics: Sendable { - public var usedBytes: UInt64 - public var inodesUsed: UInt64 + /// f_bsize: block size in bytes; the unit blocks/freeBlocks are counted in. + public var blockSize: UInt64 + /// f_blocks: total blocks in the filesystem. + public var blocks: UInt64 + /// f_bfree: free blocks in the filesystem (includes blocks reserved for root). + public var freeBlocks: UInt64 + /// f_files: total inodes in the filesystem. + public var inodes: UInt64 + /// f_ffree: free inodes in the filesystem. + public var freeInodes: UInt64 - public init(usedBytes: UInt64, inodesUsed: UInt64) { - self.usedBytes = usedBytes - self.inodesUsed = inodesUsed + public init(blockSize: UInt64, blocks: UInt64, freeBlocks: UInt64, inodes: UInt64, freeInodes: UInt64) { + self.blockSize = blockSize + self.blocks = blocks + self.freeBlocks = freeBlocks + self.inodes = inodes + self.freeInodes = freeInodes } } } diff --git a/Sources/Containerization/Vminitd.swift b/Sources/Containerization/Vminitd.swift index 8e01c2fd5..df8696045 100644 --- a/Sources/Containerization/Vminitd.swift +++ b/Sources/Containerization/Vminitd.swift @@ -180,8 +180,11 @@ extension Vminitd: VirtualMachineAgent { ) : nil, filesystem: categories.contains(.filesystem) && protoStats.hasFilesystem ? .init( - usedBytes: protoStats.filesystem.usedBytes, - inodesUsed: protoStats.filesystem.inodesUsed + blockSize: protoStats.filesystem.blockSize, + blocks: protoStats.filesystem.blocks, + freeBlocks: protoStats.filesystem.freeBlocks, + inodes: protoStats.filesystem.inodes, + freeInodes: protoStats.filesystem.freeInodes ) : nil ) } From 7e54b0f195ca4822c71803c7d67d2d6585792205 Mon Sep 17 00:00:00 2001 From: Jaewon Date: Mon, 21 Sep 2026 10:30:37 -0700 Subject: [PATCH 6/7] Update tests --- Sources/Integration/ContainerTests.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Sources/Integration/ContainerTests.swift b/Sources/Integration/ContainerTests.swift index 83fe9ef3c..8521d7ff4 100644 --- a/Sources/Integration/ContainerTests.swift +++ b/Sources/Integration/ContainerTests.swift @@ -884,20 +884,24 @@ extension IntegrationSuite { throw IntegrationError.assert(msg: "CPU usage should be > 0, got \(stats.cpu?.usageUsec ?? 0)") } - guard let filesystem = stats.filesystem, filesystem.usedBytes > 0 else { - throw IntegrationError.assert(msg: "filesystem usedBytes should be > 0, got \(stats.filesystem?.usedBytes ?? 0)") + guard let filesystem = stats.filesystem, filesystem.blocks > filesystem.freeBlocks else { + throw IntegrationError.assert( + msg: "filesystem used blocks should be > 0, got blocks=\(stats.filesystem?.blocks ?? 0) freeBlocks=\(stats.filesystem?.freeBlocks ?? 0)") } - guard filesystem.inodesUsed > 0 else { - throw IntegrationError.assert(msg: "filesystem inodesUsed should be > 0, got \(filesystem.inodesUsed)") + guard filesystem.inodes > filesystem.freeInodes else { + throw IntegrationError.assert(msg: "filesystem used inodes should be > 0, got inodes=\(filesystem.inodes) freeInodes=\(filesystem.freeInodes)") } + let usedBytes = (filesystem.blocks - filesystem.freeBlocks) * filesystem.blockSize + let inodesUsed = filesystem.inodes - filesystem.freeInodes + print("Container statistics:") print(" Processes: \(process.current)") print(" Memory: \(memory.usageBytes) bytes") print(" CPU: \(cpu.usageUsec) usec") print(" Networks: \(stats.networks?.count ?? 0) interfaces") - print(" Filesystem: \(filesystem.usedBytes) bytes, \(filesystem.inodesUsed) inodes") + print(" Filesystem: \(usedBytes) bytes, \(inodesUsed) inodes") try await container.stop() } catch { From 7cc912d8e28dd78c5227e81ff2ade0bb4387d94b Mon Sep 17 00:00:00 2001 From: Jaewon Date: Mon, 21 Sep 2026 11:13:24 -0700 Subject: [PATCH 7/7] Extend FilesystemStats to an array --- .../ContainerStatistics.swift | 12 +++-- .../SandboxContext/SandboxContext.pb.swift | 52 ++++++++++--------- .../SandboxContext/SandboxContext.proto | 18 ++++--- Sources/Containerization/Vminitd.swift | 19 ++++--- Sources/Integration/ContainerTests.swift | 4 +- .../VminitdCore/ManagedContainer.swift | 10 ++-- vminitd/Sources/VminitdCore/Server+GRPC.swift | 25 +++++---- 7 files changed, 80 insertions(+), 60 deletions(-) diff --git a/Sources/Containerization/ContainerStatistics.swift b/Sources/Containerization/ContainerStatistics.swift index 0cc3648c4..a7e0995ff 100644 --- a/Sources/Containerization/ContainerStatistics.swift +++ b/Sources/Containerization/ContainerStatistics.swift @@ -23,7 +23,7 @@ public struct ContainerStatistics: Sendable { public var blockIO: BlockIOStatistics? public var networks: [NetworkStatistics]? public var memoryEvents: MemoryEventStatistics? - public var filesystem: FilesystemStatistics? + public var filesystem: [FilesystemStatistics]? public init( id: String, @@ -33,7 +33,7 @@ public struct ContainerStatistics: Sendable { blockIO: BlockIOStatistics? = nil, networks: [NetworkStatistics]? = nil, memoryEvents: MemoryEventStatistics? = nil, - filesystem: FilesystemStatistics? = nil + filesystem: [FilesystemStatistics]? = nil ) { self.id = id self.process = process @@ -224,9 +224,10 @@ public struct ContainerStatistics: Sendable { } } - /// Filesystem occupancy for a container's writable rootfs mount. - /// Filesystem occupancy for a container's writable rootfs mount, from statfs(2). + /// Filesystem occupancy for a single mount, from statfs(2). public struct FilesystemStatistics: Sendable { + /// The mount point this entry describes. + public var mountPoint: String /// f_bsize: block size in bytes; the unit blocks/freeBlocks are counted in. public var blockSize: UInt64 /// f_blocks: total blocks in the filesystem. @@ -238,7 +239,8 @@ public struct ContainerStatistics: Sendable { /// f_ffree: free inodes in the filesystem. public var freeInodes: UInt64 - public init(blockSize: UInt64, blocks: UInt64, freeBlocks: UInt64, inodes: UInt64, freeInodes: UInt64) { + public init(mountPoint: String, blockSize: UInt64, blocks: UInt64, freeBlocks: UInt64, inodes: UInt64, freeInodes: UInt64) { + self.mountPoint = mountPoint self.blockSize = blockSize self.blocks = blocks self.freeBlocks = freeBlocks diff --git a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift index a745b1c39..173d1b11a 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift +++ b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift @@ -1599,14 +1599,10 @@ public nonisolated struct Com_Apple_Containerization_Sandbox_V3_ContainerStats: /// Clears the value of `memoryEvents`. Subsequent reads from it will return its default value. public mutating func clearMemoryEvents() {_uniqueStorage()._memoryEvents = nil} - public var filesystem: Com_Apple_Containerization_Sandbox_V3_FilesystemStats { - get {_storage._filesystem ?? Com_Apple_Containerization_Sandbox_V3_FilesystemStats()} + public var filesystem: [Com_Apple_Containerization_Sandbox_V3_FilesystemStats] { + get {_storage._filesystem} set {_uniqueStorage()._filesystem = newValue} } - /// Returns true if `filesystem` has been explicitly set. - public var hasFilesystem: Bool {_storage._filesystem != nil} - /// Clears the value of `filesystem`. Subsequent reads from it will return its default value. - public mutating func clearFilesystem() {_uniqueStorage()._filesystem = nil} public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -1781,13 +1777,16 @@ public nonisolated struct Com_Apple_Containerization_Sandbox_V3_MemoryEventStats public init() {} } -/// Raw statfs(2) fields for a container's writable rootfs mount, sufficient -/// to compute used bytes/inodes host-side. +/// Raw statfs(2) fields for a single mount, sufficient to compute used +/// bytes/inodes host-side. public nonisolated struct Com_Apple_Containerization_Sandbox_V3_FilesystemStats: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. + /// The mount point this entry describes. + public var mountPoint: String = String() + /// f_bsize: block size in bytes; the unit blocks/free_blocks are counted in. public var blockSize: UInt64 = 0 @@ -4282,7 +4281,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: Swif var _blockIo: Com_Apple_Containerization_Sandbox_V3_BlockIOStats? = nil var _networks: [Com_Apple_Containerization_Sandbox_V3_NetworkStats] = [] var _memoryEvents: Com_Apple_Containerization_Sandbox_V3_MemoryEventStats? = nil - var _filesystem: Com_Apple_Containerization_Sandbox_V3_FilesystemStats? = nil + var _filesystem: [Com_Apple_Containerization_Sandbox_V3_FilesystemStats] = [] // This property is used as the initial default value for new instances of the type. // The type itself is protecting the reference to its storage via CoW semantics. @@ -4326,7 +4325,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: Swif case 5: try { try decoder.decodeSingularMessageField(value: &_storage._blockIo) }() case 6: try { try decoder.decodeRepeatedMessageField(value: &_storage._networks) }() case 7: try { try decoder.decodeSingularMessageField(value: &_storage._memoryEvents) }() - case 8: try { try decoder.decodeSingularMessageField(value: &_storage._filesystem) }() + case 8: try { try decoder.decodeRepeatedMessageField(value: &_storage._filesystem) }() default: break } } @@ -4360,9 +4359,9 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_ContainerStats: Swif try { if let v = _storage._memoryEvents { try visitor.visitSingularMessageField(value: v, fieldNumber: 7) } }() - try { if let v = _storage._filesystem { - try visitor.visitSingularMessageField(value: v, fieldNumber: 8) - } }() + if !_storage._filesystem.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._filesystem, fieldNumber: 8) + } } try unknownFields.traverse(visitor: &visitor) } @@ -4786,7 +4785,7 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_MemoryEventStats: Sw nonisolated extension Com_Apple_Containerization_Sandbox_V3_FilesystemStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".FilesystemStats" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}block_size\0\u{1}blocks\0\u{3}free_blocks\0\u{1}inodes\0\u{3}free_inodes\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}mount_point\0\u{3}block_size\0\u{1}blocks\0\u{3}free_blocks\0\u{1}inodes\0\u{3}free_inodes\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -4794,36 +4793,41 @@ nonisolated extension Com_Apple_Containerization_Sandbox_V3_FilesystemStats: Swi // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularUInt64Field(value: &self.blockSize) }() - case 2: try { try decoder.decodeSingularUInt64Field(value: &self.blocks) }() - case 3: try { try decoder.decodeSingularUInt64Field(value: &self.freeBlocks) }() - case 4: try { try decoder.decodeSingularUInt64Field(value: &self.inodes) }() - case 5: try { try decoder.decodeSingularUInt64Field(value: &self.freeInodes) }() + case 1: try { try decoder.decodeSingularStringField(value: &self.mountPoint) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.blockSize) }() + case 3: try { try decoder.decodeSingularUInt64Field(value: &self.blocks) }() + case 4: try { try decoder.decodeSingularUInt64Field(value: &self.freeBlocks) }() + case 5: try { try decoder.decodeSingularUInt64Field(value: &self.inodes) }() + case 6: try { try decoder.decodeSingularUInt64Field(value: &self.freeInodes) }() default: break } } } public func traverse(visitor: inout V) throws { + if !self.mountPoint.isEmpty { + try visitor.visitSingularStringField(value: self.mountPoint, fieldNumber: 1) + } if self.blockSize != 0 { - try visitor.visitSingularUInt64Field(value: self.blockSize, fieldNumber: 1) + try visitor.visitSingularUInt64Field(value: self.blockSize, fieldNumber: 2) } if self.blocks != 0 { - try visitor.visitSingularUInt64Field(value: self.blocks, fieldNumber: 2) + try visitor.visitSingularUInt64Field(value: self.blocks, fieldNumber: 3) } if self.freeBlocks != 0 { - try visitor.visitSingularUInt64Field(value: self.freeBlocks, fieldNumber: 3) + try visitor.visitSingularUInt64Field(value: self.freeBlocks, fieldNumber: 4) } if self.inodes != 0 { - try visitor.visitSingularUInt64Field(value: self.inodes, fieldNumber: 4) + try visitor.visitSingularUInt64Field(value: self.inodes, fieldNumber: 5) } if self.freeInodes != 0 { - try visitor.visitSingularUInt64Field(value: self.freeInodes, fieldNumber: 5) + try visitor.visitSingularUInt64Field(value: self.freeInodes, fieldNumber: 6) } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_FilesystemStats, rhs: Com_Apple_Containerization_Sandbox_V3_FilesystemStats) -> Bool { + if lhs.mountPoint != rhs.mountPoint {return false} if lhs.blockSize != rhs.blockSize {return false} if lhs.blocks != rhs.blocks {return false} if lhs.freeBlocks != rhs.freeBlocks {return false} diff --git a/Sources/Containerization/SandboxContext/SandboxContext.proto b/Sources/Containerization/SandboxContext/SandboxContext.proto index 15889203a..a7c6cf418 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.proto +++ b/Sources/Containerization/SandboxContext/SandboxContext.proto @@ -428,7 +428,7 @@ message ContainerStats { BlockIOStats block_io = 5; repeated NetworkStats networks = 6; MemoryEventStats memory_events = 7; - FilesystemStats filesystem = 8; + repeated FilesystemStats filesystem = 8; } message ProcessStats { @@ -503,17 +503,19 @@ message MemoryEventStats { uint64 oom_group_kill = 6; } -// Raw statfs(2) fields for a container's writable rootfs mount, sufficient -// to compute used bytes/inodes host-side. +// Raw statfs(2) fields for a single mount, sufficient to compute used +// bytes/inodes host-side. message FilesystemStats { + // The mount point this entry describes. + string mount_point = 1; // f_bsize: block size in bytes; the unit blocks/free_blocks are counted in. - uint64 block_size = 1; + uint64 block_size = 2; // f_blocks: total blocks in the filesystem. - uint64 blocks = 2; + uint64 blocks = 3; // f_bfree: free blocks in the filesystem (includes blocks reserved for root). - uint64 free_blocks = 3; + uint64 free_blocks = 4; // f_files: total inodes in the filesystem. - uint64 inodes = 4; + uint64 inodes = 5; // f_ffree: free inodes in the filesystem. - uint64 free_inodes = 5; + uint64 free_inodes = 6; } diff --git a/Sources/Containerization/Vminitd.swift b/Sources/Containerization/Vminitd.swift index df8696045..9bf7c080d 100644 --- a/Sources/Containerization/Vminitd.swift +++ b/Sources/Containerization/Vminitd.swift @@ -178,14 +178,17 @@ extension Vminitd: VirtualMachineAgent { oom: protoStats.memoryEvents.oom, oomKill: protoStats.memoryEvents.oomKill ) : nil, - filesystem: categories.contains(.filesystem) && protoStats.hasFilesystem - ? .init( - blockSize: protoStats.filesystem.blockSize, - blocks: protoStats.filesystem.blocks, - freeBlocks: protoStats.filesystem.freeBlocks, - inodes: protoStats.filesystem.inodes, - freeInodes: protoStats.filesystem.freeInodes - ) : nil + filesystem: categories.contains(.filesystem) + ? protoStats.filesystem.map { entry in + ContainerStatistics.FilesystemStatistics( + mountPoint: entry.mountPoint, + blockSize: entry.blockSize, + blocks: entry.blocks, + freeBlocks: entry.freeBlocks, + inodes: entry.inodes, + freeInodes: entry.freeInodes + ) + } : nil ) } } diff --git a/Sources/Integration/ContainerTests.swift b/Sources/Integration/ContainerTests.swift index 8521d7ff4..79c3a209f 100644 --- a/Sources/Integration/ContainerTests.swift +++ b/Sources/Integration/ContainerTests.swift @@ -884,9 +884,9 @@ extension IntegrationSuite { throw IntegrationError.assert(msg: "CPU usage should be > 0, got \(stats.cpu?.usageUsec ?? 0)") } - guard let filesystem = stats.filesystem, filesystem.blocks > filesystem.freeBlocks else { + guard let filesystem = stats.filesystem?.first, filesystem.blocks > filesystem.freeBlocks else { throw IntegrationError.assert( - msg: "filesystem used blocks should be > 0, got blocks=\(stats.filesystem?.blocks ?? 0) freeBlocks=\(stats.filesystem?.freeBlocks ?? 0)") + msg: "filesystem used blocks should be > 0, got blocks=\(stats.filesystem?.first?.blocks ?? 0) freeBlocks=\(stats.filesystem?.first?.freeBlocks ?? 0)") } guard filesystem.inodes > filesystem.freeInodes else { diff --git a/vminitd/Sources/VminitdCore/ManagedContainer.swift b/vminitd/Sources/VminitdCore/ManagedContainer.swift index 776e279b6..e2e0b7751 100644 --- a/vminitd/Sources/VminitdCore/ManagedContainer.swift +++ b/vminitd/Sources/VminitdCore/ManagedContainer.swift @@ -38,6 +38,10 @@ public actor ManagedContainer { self.initProcess.pid } + var rootfsPath: String { + self.bundle.rootfsPath.path + } + init( id: String, stdio: HostStdio, @@ -240,12 +244,12 @@ extension ManagedContainer { try self.cgroupManager.getMemoryEvents() } - func filesystemStats() throws -> CZ_Statfs { + func filesystemStats(of mount: String) throws -> CZ_Statfs { var s = CZ_Statfs() - guard CZ_statfs(self.bundle.rootfsPath.path, &s) == 0 else { + guard CZ_statfs(mount, &s) == 0 else { throw ContainerizationError( .internalError, - message: "statfs(\(self.bundle.rootfsPath.path)) failed: errno \(errno)" + message: "statfs(\(mount)) failed: errno \(errno)" ) } return s diff --git a/vminitd/Sources/VminitdCore/Server+GRPC.swift b/vminitd/Sources/VminitdCore/Server+GRPC.swift index 6bd2c19b4..4b58bbb87 100644 --- a/vminitd/Sources/VminitdCore/Server+GRPC.swift +++ b/vminitd/Sources/VminitdCore/Server+GRPC.swift @@ -1725,9 +1725,11 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ } // Get filesystem usage only if requested - var filesystemStats: CZ_Statfs? + var filesystemStats: [(mountPoint: String, stat: CZ_Statfs)] = [] if wantFilesystem { - filesystemStats = try await container.filesystemStats() + let mountPoint = await container.rootfsPath + let stat = try await container.filesystemStats(of: mountPoint) + filesystemStats = [(mountPoint, stat)] } containerStats.append( @@ -1809,7 +1811,7 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ cgStats: Cgroup2Stats?, networkStats: [Com_Apple_Containerization_Sandbox_V3_NetworkStats], memoryEvents: MemoryEvents?, - filesystemStats: CZ_Statfs?, + filesystemStats: [(mountPoint: String, stat: CZ_Statfs)], wantProcess: Bool, wantMemory: Bool, wantCPU: Bool, @@ -1889,13 +1891,16 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ } } - if wantFilesystem, let fs = filesystemStats { - $0.filesystem = .with { - $0.blockSize = fs.f_bsize - $0.blocks = fs.f_blocks - $0.freeBlocks = fs.f_bfree - $0.inodes = fs.f_files - $0.freeInodes = fs.f_ffree + if wantFilesystem { + $0.filesystem = filesystemStats.map { entry in + .with { + $0.mountPoint = entry.mountPoint + $0.blockSize = entry.stat.f_bsize + $0.blocks = entry.stat.f_blocks + $0.freeBlocks = entry.stat.f_bfree + $0.inodes = entry.stat.f_files + $0.freeInodes = entry.stat.f_ffree + } } } }