Post

Replies

Boosts

Views

Activity

Reply to OSSystemExtensionRequestDelegate doesn't get a callback; XPC connection cancelled
Thank you very much! The delegate was indeed getting deallocated early. I didn't realize the request only makes a weak reference to the delegate. I wonder if you could pass a bit of feedback to the docs team: from this class-level view, the delegate property contains no hints that the reference is weak. It's only when you drill down to the property that you can see it's a weak ref.
Topic: App & System Services SubTopic: Core OS Tags:
Jan ’25
Reply to OSSystemExtensionRequestDelegate doesn't get a callback; XPC connection cancelled
The SimpleFirewall application in the Filtering Network Traffic example works fine: I built and ran it on the same system, and I can see the request delegate getting callbacks in the example application. With respect to the log snippet you posted, is Coder Desktop your app? Yes, that's correct. import Foundation import os import SystemExtensions enum SystemExtensionState: Equatable, Sendable { case uninstalled case needsUserApproval case installed case failed(String) var description: String { switch self { case .uninstalled: return "VPN SystemExtension is waiting to be activated" case .needsUserApproval: return "VPN SystemExtension needs user approval to activate" case .installed: return "VPN SystemExtension is installed" case let .failed(error): return "VPN SystemExtension failed with error: \(error)" } } } protocol SystemExtensionAsyncRecorder: Sendable { func recordSystemExtensionState(_ state: SystemExtensionState) async } extension CoderVPNService: SystemExtensionAsyncRecorder { func recordSystemExtensionState(_ state: SystemExtensionState) async { sysExtnState = state } var extensionBundle: Bundle { let extensionsDirectoryURL = URL( fileURLWithPath: "Contents/Library/SystemExtensions", relativeTo: Bundle.main.bundleURL ) let extensionURLs: [URL] do { extensionURLs = try FileManager.default.contentsOfDirectory(at: extensionsDirectoryURL, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) } catch { fatalError("Failed to get the contents of " + "\(extensionsDirectoryURL.absoluteString): \(error.localizedDescription)") } // here we're just going to assume that there is only ever going to be one SystemExtension // packaged up in the application bundle. If we ever need to ship multiple versions or have // multiple extensions, we'll need to revisit this assumption. guard let extensionURL = extensionURLs.first else { fatalError("Failed to find any system extensions") } guard let extensionBundle = Bundle(url: extensionURL) else { fatalError("Failed to create a bundle with URL \(extensionURL.absoluteString)") } return extensionBundle } func installSystemExtension() { logger.info("activating SystemExtension") guard let bundleID = extensionBundle.bundleIdentifier else { logger.error("Bundle has no identifier") return } let request = OSSystemExtensionRequest.activationRequest( forExtensionWithIdentifier: bundleID, queue: .main ) let delegate = SystemExtensionDelegate(asyncDelegate: self) request.delegate = delegate OSSystemExtensionManager.shared.submitRequest(request) logger.info("submitted SystemExtension request with bundleID: \(bundleID)") } } /// A delegate for the OSSystemExtensionRequest that maps the callbacks to async calls on the /// AsyncDelegate (CoderVPNService in production). class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>: NSObject, OSSystemExtensionRequestDelegate { private var logger = Logger(subsystem: "com.coder.Coder-Desktop", category: "vpn-installer") private var asyncDelegate: AsyncDelegate init(asyncDelegate: AsyncDelegate) { self.asyncDelegate = asyncDelegate super.init() logger.info("SystemExtensionDelegate initialized") } func request( _: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result ) { guard result == .completed else { logger.error("Unexpected result \(result.rawValue) for system extension request") let state = SystemExtensionState.failed("system extension not installed: \(result.rawValue)") Task { [asyncDelegate] in await asyncDelegate.recordSystemExtensionState(state) } return } logger.info("SystemExtension activated") Task { [asyncDelegate] in await asyncDelegate.recordSystemExtensionState(SystemExtensionState.installed) } } func request(_: OSSystemExtensionRequest, didFailWithError error: Error) { logger.error("System extension request failed: \(error.localizedDescription)") Task { [asyncDelegate] in await asyncDelegate.recordSystemExtensionState( SystemExtensionState.failed(error.localizedDescription)) } } func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) { logger.error("Extension \(request.identifier) requires user approval") Task { [asyncDelegate] in await asyncDelegate.recordSystemExtensionState(SystemExtensionState.needsUserApproval) } } func request( _ request: OSSystemExtensionRequest, actionForReplacingExtension existing: OSSystemExtensionProperties, withExtension extension: OSSystemExtensionProperties ) -> OSSystemExtensionRequest.ReplacementAction { // swiftlint: disable line_length logger.info("Replacing \(request.identifier) v\(existing.bundleShortVersion) with v\(`extension`.bundleShortVersion)") // swiftlint: enable line_length return .replace } } My CoderVPNService is an @MainActor class, if it matters, and this extension to it and the accompanying delegate handle the system extension request and reporting back status asynchronously.
Topic: App & System Services SubTopic: Core OS Tags:
Jan ’25
Reply to mDNSResponder suppressing queries from a spawned process
There are private SPIs that explicitly manage this but the "default" behavior is that a newly created process simply inherits the sandbox of it's parent process*. Is DNS policy enforced by mDNSResponder part of the Sandbox, or is DNS policy a different thing? If it's the Sandbox, and the child process inherits the same profile, then how can DNS resolve correctly in the parent but not the child? At a code leve, you can actually see the "blocked by policy" error in mDNSReponder's code.. In your particular case, it's almost certainly because of something about how your packet tunnel provider is configured which is then being inherited by your new child process Right, so I took a look at the open source mDNSResponder, and this appears to be where it blocks queries. The problem for me in understanding what's happening is that this function calls into undocumented APIs, creating something called a path evaluator, which then spits out a verdict of "satisfied" or "unsatisfied" for the DNS query. Do you know what this function actually does and how it decides whether the path is satisfied? In collecting parameters for this evaluation query, it (again via an undocumented API) collects a "UUID" associated with the PID of the querier. Do you know what this UUID is? I've tried searching Mac docs for any mention of a process UUID, but have so far come up empty. Even when the VPN is enabled, DNS resolution seems to work fine when I run the same binary from a Terminal, but fails when spawned from the system extension. I'm certainly not confident that my tunnel configuration itself has nothing to do with it, but it happens regardless of whether I add any DNS settings to the tunnel configuration or not. I'm flying blind here because I haven't found any information about how mDNSResponder decides what is and is not blocked by policy, and how tunnel DNS configuration might enter in the mix. Why are you spawning a new process and what are you actually trying to do? I'm concerned that you're straying outside the guidance in TN3120, which rarely goes well. I'm spawning a new process because the code that actually implements the tunnel is written in Go (it's essentially Wireguard), and I don't want to have Swift threads/memory management alongside Go goroutines & garbage collection in the same process. I'm following the guidance of TN3120---the goal really is a packet tunnel. The spawned process tries to dial the VPN server by DNS name and that's where it's getting hung up. As context, what are you actually trying to resolve (bonjour or standard DNS) and what networking API are you actually using? Have you actually tested the same code in your extension (not just the same resolution with a different API)? In the System Extension process, I've tried both the URLSession API to reach the VPN server by name and the lower level POSIX getaddrinfo. These resolve the standard DNS name correctly, and I can see mDNSResponder dropping logs about the query. The logs look the same as the ones dropped when the spawned process makes the query, other than the spawned process gets "blocked by policy." I'm pretty sure Go uses getaddrinfo, but I can try to double check it's the same API. I've also tried using the standard Swift Process() call to spawn the child from the System Extension, and I get the same results regarding DNS (the reason I'm using posix_spawn is to pass the TUN file descriptor, Process() only supports stdin/stdout/stderr file descriptors).
Aug ’24
Reply to mDNSResponder suppressing queries from a spawned process
Are you sure you're extension isn't what's blocking things? What's the network extension type and what policies/behavior does it have? The system extension is a PacketTunnelProvider, setting up a custom VPN. It doesn't do any filtering / policy. Any tool your extension spawned would have been treated the same as the "broader" system, NOT you network extension. So that's the strangest part about this: if I run the same binary from a terminal, it resolves DNS and connects just fine, including when run as root. It's only when spawned from my system extension that DNS queries are "blocked by policy." At this point I'm still trying to understand DNS policy at the highest level: where do the policies come from? Are they baked into the system like sandbox policies, or are they configurable via some API?
Aug ’24
Reply to calling posix_spawn from a network System Extension
Well, yes. "application.sb" is the "base" sandbox used by all applications, so there are going to be PLENTY of execution paths it allows. That's definitely not the sandbox profile app extension hosts would typically use. They are, but this also works very differently than a standard "sandboxed app". The issue here is that the "owner" of an extension point is responsible for determining what your extension can/cannot do, NOT the app extension itself. Critically, your app extension cannot "expand" it's own capabilities beyond what the extension point "owner" (in this caes, then network extension system) defined. I'm sorry, but again, are you sure that's the case, or is this another instance of what you "expect" to be? In my testing, I'm able to exec files in /var/root/Downloads if I set Downloads to read/write in the signing capabilities for my System Extension, and unable to exec if I don't set this permission. In applications.sb I see (when (or (entitlement "com.apple.security.files.downloads.read-only") (entitlement "com.apple.security.files.downloads.read-write")) (allow process-exec (home-subpath "/Downloads"))) but nothing like this appears in system.sb or com.apple.neagent.sb. I'd really like someone from the network extension team to weigh in, if possible.
Topic: App & System Services SubTopic: Core OS Tags:
Aug ’24
Reply to calling posix_spawn from a network System Extension
Is it possible to execute other processes from within the System Extension sandbox? No, at least not through posix_spawn (or fork/exec). It's possible you might be allowed to launch an XPCService, but I suspect that will fail as well. I've been continuing to experiment, and I've found I can exec binaries located in /Library/, so I don't believe this information is accurate. I ended up spelunking into /System/Library/Sandbox/application.sb to try to understand what is and is not allowed while sandboxed because I could not find any official documentation. There appear to be several paths that allow exec. this also works very differently than a standard "sandboxed app". The issue here is that the "owner" of an extension point is responsible for determining what your extension can/cannot do, NOT the app extension itself. Critically, your app extension cannot "expand" it's own capabilities beyond what the extension point "owner" (in this caes, then network extension system) defined. Is there any documentation that describes how the extension point sandboxing works, i.e. which profile is used?
Topic: App & System Services SubTopic: Core OS Tags:
Aug ’24
Reply to OSSystemExtensionRequestDelegate doesn't get a callback; XPC connection cancelled
Bug report, for posterity: FB16390870
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
Jan ’25
Reply to OSSystemExtensionRequestDelegate doesn't get a callback; XPC connection cancelled
Thank you very much! The delegate was indeed getting deallocated early. I didn't realize the request only makes a weak reference to the delegate. I wonder if you could pass a bit of feedback to the docs team: from this class-level view, the delegate property contains no hints that the reference is weak. It's only when you drill down to the property that you can see it's a weak ref.
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
Jan ’25
Reply to OSSystemExtensionRequestDelegate doesn't get a callback; XPC connection cancelled
The SimpleFirewall application in the Filtering Network Traffic example works fine: I built and ran it on the same system, and I can see the request delegate getting callbacks in the example application. With respect to the log snippet you posted, is Coder Desktop your app? Yes, that's correct. import Foundation import os import SystemExtensions enum SystemExtensionState: Equatable, Sendable { case uninstalled case needsUserApproval case installed case failed(String) var description: String { switch self { case .uninstalled: return "VPN SystemExtension is waiting to be activated" case .needsUserApproval: return "VPN SystemExtension needs user approval to activate" case .installed: return "VPN SystemExtension is installed" case let .failed(error): return "VPN SystemExtension failed with error: \(error)" } } } protocol SystemExtensionAsyncRecorder: Sendable { func recordSystemExtensionState(_ state: SystemExtensionState) async } extension CoderVPNService: SystemExtensionAsyncRecorder { func recordSystemExtensionState(_ state: SystemExtensionState) async { sysExtnState = state } var extensionBundle: Bundle { let extensionsDirectoryURL = URL( fileURLWithPath: "Contents/Library/SystemExtensions", relativeTo: Bundle.main.bundleURL ) let extensionURLs: [URL] do { extensionURLs = try FileManager.default.contentsOfDirectory(at: extensionsDirectoryURL, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) } catch { fatalError("Failed to get the contents of " + "\(extensionsDirectoryURL.absoluteString): \(error.localizedDescription)") } // here we're just going to assume that there is only ever going to be one SystemExtension // packaged up in the application bundle. If we ever need to ship multiple versions or have // multiple extensions, we'll need to revisit this assumption. guard let extensionURL = extensionURLs.first else { fatalError("Failed to find any system extensions") } guard let extensionBundle = Bundle(url: extensionURL) else { fatalError("Failed to create a bundle with URL \(extensionURL.absoluteString)") } return extensionBundle } func installSystemExtension() { logger.info("activating SystemExtension") guard let bundleID = extensionBundle.bundleIdentifier else { logger.error("Bundle has no identifier") return } let request = OSSystemExtensionRequest.activationRequest( forExtensionWithIdentifier: bundleID, queue: .main ) let delegate = SystemExtensionDelegate(asyncDelegate: self) request.delegate = delegate OSSystemExtensionManager.shared.submitRequest(request) logger.info("submitted SystemExtension request with bundleID: \(bundleID)") } } /// A delegate for the OSSystemExtensionRequest that maps the callbacks to async calls on the /// AsyncDelegate (CoderVPNService in production). class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>: NSObject, OSSystemExtensionRequestDelegate { private var logger = Logger(subsystem: "com.coder.Coder-Desktop", category: "vpn-installer") private var asyncDelegate: AsyncDelegate init(asyncDelegate: AsyncDelegate) { self.asyncDelegate = asyncDelegate super.init() logger.info("SystemExtensionDelegate initialized") } func request( _: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result ) { guard result == .completed else { logger.error("Unexpected result \(result.rawValue) for system extension request") let state = SystemExtensionState.failed("system extension not installed: \(result.rawValue)") Task { [asyncDelegate] in await asyncDelegate.recordSystemExtensionState(state) } return } logger.info("SystemExtension activated") Task { [asyncDelegate] in await asyncDelegate.recordSystemExtensionState(SystemExtensionState.installed) } } func request(_: OSSystemExtensionRequest, didFailWithError error: Error) { logger.error("System extension request failed: \(error.localizedDescription)") Task { [asyncDelegate] in await asyncDelegate.recordSystemExtensionState( SystemExtensionState.failed(error.localizedDescription)) } } func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) { logger.error("Extension \(request.identifier) requires user approval") Task { [asyncDelegate] in await asyncDelegate.recordSystemExtensionState(SystemExtensionState.needsUserApproval) } } func request( _ request: OSSystemExtensionRequest, actionForReplacingExtension existing: OSSystemExtensionProperties, withExtension extension: OSSystemExtensionProperties ) -> OSSystemExtensionRequest.ReplacementAction { // swiftlint: disable line_length logger.info("Replacing \(request.identifier) v\(existing.bundleShortVersion) with v\(`extension`.bundleShortVersion)") // swiftlint: enable line_length return .replace } } My CoderVPNService is an @MainActor class, if it matters, and this extension to it and the accompanying delegate handle the system extension request and reporting back status asynchronously.
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
Jan ’25
Reply to OSSystemExtensionRequestDelegate doesn't get a callback; XPC connection cancelled
Yes, this is a standard GUI app, the @main is a SwiftUI App struct. I've been launching it from XCode, though.
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
Jan ’25
Reply to mDNSResponder suppressing queries from a spawned process
There are private SPIs that explicitly manage this but the "default" behavior is that a newly created process simply inherits the sandbox of it's parent process*. Is DNS policy enforced by mDNSResponder part of the Sandbox, or is DNS policy a different thing? If it's the Sandbox, and the child process inherits the same profile, then how can DNS resolve correctly in the parent but not the child? At a code leve, you can actually see the "blocked by policy" error in mDNSReponder's code.. In your particular case, it's almost certainly because of something about how your packet tunnel provider is configured which is then being inherited by your new child process Right, so I took a look at the open source mDNSResponder, and this appears to be where it blocks queries. The problem for me in understanding what's happening is that this function calls into undocumented APIs, creating something called a path evaluator, which then spits out a verdict of "satisfied" or "unsatisfied" for the DNS query. Do you know what this function actually does and how it decides whether the path is satisfied? In collecting parameters for this evaluation query, it (again via an undocumented API) collects a "UUID" associated with the PID of the querier. Do you know what this UUID is? I've tried searching Mac docs for any mention of a process UUID, but have so far come up empty. Even when the VPN is enabled, DNS resolution seems to work fine when I run the same binary from a Terminal, but fails when spawned from the system extension. I'm certainly not confident that my tunnel configuration itself has nothing to do with it, but it happens regardless of whether I add any DNS settings to the tunnel configuration or not. I'm flying blind here because I haven't found any information about how mDNSResponder decides what is and is not blocked by policy, and how tunnel DNS configuration might enter in the mix. Why are you spawning a new process and what are you actually trying to do? I'm concerned that you're straying outside the guidance in TN3120, which rarely goes well. I'm spawning a new process because the code that actually implements the tunnel is written in Go (it's essentially Wireguard), and I don't want to have Swift threads/memory management alongside Go goroutines & garbage collection in the same process. I'm following the guidance of TN3120---the goal really is a packet tunnel. The spawned process tries to dial the VPN server by DNS name and that's where it's getting hung up. As context, what are you actually trying to resolve (bonjour or standard DNS) and what networking API are you actually using? Have you actually tested the same code in your extension (not just the same resolution with a different API)? In the System Extension process, I've tried both the URLSession API to reach the VPN server by name and the lower level POSIX getaddrinfo. These resolve the standard DNS name correctly, and I can see mDNSResponder dropping logs about the query. The logs look the same as the ones dropped when the spawned process makes the query, other than the spawned process gets "blocked by policy." I'm pretty sure Go uses getaddrinfo, but I can try to double check it's the same API. I've also tried using the standard Swift Process() call to spawn the child from the System Extension, and I get the same results regarding DNS (the reason I'm using posix_spawn is to pass the TUN file descriptor, Process() only supports stdin/stdout/stderr file descriptors).
Replies
Boosts
Views
Activity
Aug ’24
Reply to mDNSResponder suppressing queries from a spawned process
Are you sure you're extension isn't what's blocking things? What's the network extension type and what policies/behavior does it have? The system extension is a PacketTunnelProvider, setting up a custom VPN. It doesn't do any filtering / policy. Any tool your extension spawned would have been treated the same as the "broader" system, NOT you network extension. So that's the strangest part about this: if I run the same binary from a terminal, it resolves DNS and connects just fine, including when run as root. It's only when spawned from my system extension that DNS queries are "blocked by policy." At this point I'm still trying to understand DNS policy at the highest level: where do the policies come from? Are they baked into the system like sandbox policies, or are they configurable via some API?
Replies
Boosts
Views
Activity
Aug ’24
Reply to calling posix_spawn from a network System Extension
Well, yes. "application.sb" is the "base" sandbox used by all applications, so there are going to be PLENTY of execution paths it allows. That's definitely not the sandbox profile app extension hosts would typically use. They are, but this also works very differently than a standard "sandboxed app". The issue here is that the "owner" of an extension point is responsible for determining what your extension can/cannot do, NOT the app extension itself. Critically, your app extension cannot "expand" it's own capabilities beyond what the extension point "owner" (in this caes, then network extension system) defined. I'm sorry, but again, are you sure that's the case, or is this another instance of what you "expect" to be? In my testing, I'm able to exec files in /var/root/Downloads if I set Downloads to read/write in the signing capabilities for my System Extension, and unable to exec if I don't set this permission. In applications.sb I see (when (or (entitlement "com.apple.security.files.downloads.read-only") (entitlement "com.apple.security.files.downloads.read-write")) (allow process-exec (home-subpath "/Downloads"))) but nothing like this appears in system.sb or com.apple.neagent.sb. I'd really like someone from the network extension team to weigh in, if possible.
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
Aug ’24
Reply to calling posix_spawn from a network System Extension
Is it possible to execute other processes from within the System Extension sandbox? No, at least not through posix_spawn (or fork/exec). It's possible you might be allowed to launch an XPCService, but I suspect that will fail as well. I've been continuing to experiment, and I've found I can exec binaries located in /Library/, so I don't believe this information is accurate. I ended up spelunking into /System/Library/Sandbox/application.sb to try to understand what is and is not allowed while sandboxed because I could not find any official documentation. There appear to be several paths that allow exec. this also works very differently than a standard "sandboxed app". The issue here is that the "owner" of an extension point is responsible for determining what your extension can/cannot do, NOT the app extension itself. Critically, your app extension cannot "expand" it's own capabilities beyond what the extension point "owner" (in this caes, then network extension system) defined. Is there any documentation that describes how the extension point sandboxing works, i.e. which profile is used?
Topic: App & System Services SubTopic: Core OS Tags:
Replies
Boosts
Views
Activity
Aug ’24