1414// limitations under the License.
1515//===----------------------------------------------------------------------===//
1616
17+ import Containerization
1718import ContainerizationError
1819import ContainerizationOCI
1920import ContainerizationOS
2021import Foundation
2122import Logging
23+ import Synchronization
24+
25+ #if canImport(Musl)
26+ import Musl
27+ #elseif canImport(Glibc)
28+ import Glibc
29+ #endif
2230
2331actor ManagedContainer {
2432 let id : String
@@ -28,6 +36,131 @@ actor ManagedContainer {
2836 private let log : Logger
2937 private let bundle : ContainerizationOCI . Bundle
3038 private var execs : [ String : ManagedProcess ] = [ : ]
39+ private var namespaceWorker : NamespaceWorker ?
40+
41+ /// Worker thread that runs in container's namespace for filesystem operations
42+ private final class NamespaceWorker : @unchecked Sendable {
43+ private let containerID : String
44+ private let containerPID : Int32
45+ private var workerThread : Thread ?
46+ private let eventQueue : Mutex < [ FileSystemEvent ] > = Mutex ( [ ] )
47+ private let semaphore : DispatchSemaphore = DispatchSemaphore ( value: 0 )
48+ private let shouldStop : Atomic < Bool > = Atomic ( false )
49+
50+ struct FileSystemEvent : Sendable {
51+ let path : String
52+ let eventType : Com_Apple_Containerization_Sandbox_V3_FileSystemEventType
53+ let completion : @Sendable ( Result < Void , Error > ) -> Void
54+ }
55+
56+ init ( containerID: String , containerPID: Int32 ) {
57+ self . containerID = containerID
58+ self . containerPID = containerPID
59+ }
60+
61+ func start( ) throws {
62+ guard workerThread == nil else {
63+ throw ContainerizationError ( . invalidState, message: " NamespaceWorker already started " )
64+ }
65+
66+ let thread = Thread { [ weak self] in
67+ self ? . runWorkerLoop ( )
68+ }
69+ thread. name = " namespace-worker- \( containerID) "
70+ workerThread = thread
71+ thread. start ( )
72+ }
73+
74+ func enqueueEvent( path: String , eventType: Com_Apple_Containerization_Sandbox_V3_FileSystemEventType ) async throws {
75+ try await withCheckedThrowingContinuation { continuation in
76+ let event = FileSystemEvent (
77+ path: path,
78+ eventType: eventType,
79+ completion: { @Sendable result in
80+ continuation. resume ( with: result)
81+ }
82+ )
83+
84+ eventQueue. withLock { ( queue: inout [ FileSystemEvent ] ) in
85+ queue. append ( event)
86+ }
87+ semaphore. signal ( )
88+ }
89+ }
90+
91+ func stop( ) {
92+ shouldStop. store ( true , ordering: . relaxed)
93+ semaphore. signal ( ) // Wake up the worker thread
94+ workerThread? . cancel ( )
95+ workerThread = nil
96+ }
97+
98+ private func runWorkerLoop( ) {
99+ // Enter container namespace
100+ do {
101+ try enterContainerNamespace ( )
102+ } catch {
103+ return
104+ }
105+
106+ // Worker loop
107+ while !shouldStop. load ( ordering: . relaxed) {
108+ semaphore. wait ( )
109+
110+ // Check stop condition again after waking up
111+ if shouldStop. load ( ordering: . relaxed) {
112+ break
113+ }
114+
115+ // Process all queued events
116+ let events = eventQueue. withLock { ( queue: inout [ FileSystemEvent ] ) -> [ FileSystemEvent ] in
117+ let currentEvents = Array ( queue)
118+ queue. removeAll ( )
119+ return currentEvents
120+ }
121+
122+ for event in events {
123+ do {
124+ try generateSyntheticInotifyEvent ( path: event. path, eventType: event. eventType)
125+ event. completion ( . success( ( ) ) )
126+ } catch {
127+ event. completion ( . failure( error) )
128+ }
129+ }
130+ }
131+ }
132+
133+ private func enterContainerNamespace( ) throws {
134+ let nsPath = " /proc/ \( containerPID) /ns/mnt "
135+ let fd = open ( nsPath, O_RDONLY)
136+ guard fd >= 0 else {
137+ throw ContainerizationError ( . internalError, message: " Failed to open namespace file: \( nsPath) " )
138+ }
139+ defer { _ = close ( fd) }
140+
141+ guard setns ( fd, CLONE_NEWNS) == 0 else {
142+ throw ContainerizationError ( . internalError, message: " Failed to setns to mount namespace: \( errno) " )
143+ }
144+ }
145+
146+ private func generateSyntheticInotifyEvent(
147+ path: String ,
148+ eventType: Com_Apple_Containerization_Sandbox_V3_FileSystemEventType
149+ ) throws {
150+ if eventType == . delete && !FileManager. default. fileExists ( atPath: path) {
151+ return
152+ }
153+
154+ let attributes = try FileManager . default. attributesOfItem ( atPath: path)
155+ guard let permissions = attributes [ . posixPermissions] as? NSNumber else {
156+ throw ContainerizationError ( . internalError, message: " Failed to get file permissions for path: \( path) " )
157+ }
158+ try FileManager . default. setAttributes (
159+ [ . posixPermissions: permissions] ,
160+ ofItemAtPath: path
161+ )
162+ }
163+ }
31164
32165 var pid : Int32 {
33166 self . initProcess. pid
@@ -79,6 +212,9 @@ actor ManagedContainer {
79212 self . id = id
80213 self . bundle = bundle
81214 self . log = log
215+
216+ // Initialize namespace worker - will be started after process starts
217+ self . namespaceWorker = nil
82218 } catch {
83219 try ? cgManager. delete ( )
84220 throw error
@@ -96,6 +232,26 @@ extension ManagedContainer {
96232 }
97233 }
98234
235+ /// Start namespace worker thread after container process starts
236+ private func startNamespaceWorker( ) throws {
237+ let pid = self . initProcess. pid
238+ guard pid > 0 else {
239+ throw ContainerizationError ( . invalidState, message: " Container process not started " )
240+ }
241+
242+ let worker = NamespaceWorker ( containerID: self . id, containerPID: pid)
243+ try worker. start ( )
244+ self . namespaceWorker = worker
245+ }
246+
247+ /// Execute filesystem event using dedicated namespace thread
248+ func executeFileSystemEvent( path: String , eventType: Com_Apple_Containerization_Sandbox_V3_FileSystemEventType ) async throws {
249+ guard let worker = self . namespaceWorker else {
250+ throw ContainerizationError ( . invalidState, message: " Namespace worker not started for container \( self . id) " )
251+ }
252+ try await worker. enqueueEvent ( path: path, eventType: eventType)
253+ }
254+
99255 func createExec(
100256 id: String ,
101257 stdio: HostStdio ,
@@ -121,7 +277,14 @@ extension ManagedContainer {
121277
122278 func start( execID: String ) async throws -> Int32 {
123279 let proc = try self . getExecOrInit ( execID: execID)
124- return try await ProcessSupervisor . default. start ( process: proc)
280+ let pid = try await ProcessSupervisor . default. start ( process: proc)
281+
282+ // Start namespace worker thread if this is the init process
283+ if execID == self . id {
284+ try self . startNamespaceWorker ( )
285+ }
286+
287+ return pid
125288 }
126289
127290 func wait( execID: String ) async throws -> ManagedProcess . ExitStatus {
@@ -155,6 +318,10 @@ extension ManagedContainer {
155318 }
156319
157320 func delete( ) throws {
321+ // Stop namespace worker thread
322+ self . namespaceWorker? . stop ( )
323+ self . namespaceWorker = nil
324+
158325 try self . bundle. delete ( )
159326 try self . cgroupManager. delete ( force: true )
160327 }
0 commit comments