Ver código fonte

process: hold token to avoid XPC service termination

When under memory pressure, XNU can decide to terminate the XPC helper
unless there is a pending response. We force it to have a pending response
by providing a NOP callback which will only be triggered after QEMU
terminates.

Fixes #5489
osy 2 anos atrás
pai
commit
d5a9811f0c
3 arquivos alterados com 36 adições e 2 exclusões
  1. 24 2
      QEMUHelper/QEMUHelper.m
  2. 8 0
      QEMUHelper/QEMUHelperProtocol.h
  3. 4 0
      Services/UTMProcess.m

+ 24 - 2
QEMUHelper/QEMUHelper.m

@@ -20,8 +20,9 @@
 
 @interface QEMUHelper ()
 
-@property NSMutableArray<NSURL *> *urls;
-@property NSTask *childTask;
+@property (nonatomic) NSMutableArray<NSURL *> *urls;
+@property (nonatomic, nullable) NSTask *childTask;
+@property (nonatomic, nullable) tokenCallback_t activeToken;
 
 @end
 
@@ -136,9 +137,11 @@
     task.terminationHandler = ^(NSTask *task) {
         _self.childTask = nil;
         [_self.connection.remoteObjectProxy processHasExited:task.terminationStatus message:nil];
+        [_self invalidateToken];
     };
     if (![task launchAndReturnError:&err]) {
         NSLog(@"Error starting QEMU: %@", err);
+        [self invalidateToken];
         completion(NO, err.localizedDescription);
     } else {
         self.childTask = task;
@@ -149,6 +152,25 @@
 - (void)terminate {
     [self.childTask terminate];
     self.childTask = nil;
+    [self invalidateToken];
+}
+
+- (void)assertActiveWithToken:(tokenCallback_t)token {
+    @synchronized (self) {
+        if (self.activeToken) {
+            self.activeToken(NO);
+        }
+        self.activeToken = token;
+    }
+}
+
+- (void)invalidateToken {
+    @synchronized (self) {
+        if (self.activeToken) {
+            self.activeToken(YES);
+        }
+        self.activeToken = nil;
+    }
 }
 
 @end

+ 8 - 0
QEMUHelper/QEMUHelperProtocol.h

@@ -16,6 +16,8 @@
 
 #import <Foundation/Foundation.h>
 
+typedef void (^tokenCallback_t)(BOOL);
+
 NS_ASSUME_NONNULL_BEGIN
 
 // The protocol that this service will vend as its API. This header file will also need to be visible to the process hosting the service.
@@ -29,6 +31,12 @@ NS_ASSUME_NONNULL_BEGIN
 - (void)startQemu:(NSString *)binName standardOutput:(NSFileHandle *)standardOutput standardError:(NSFileHandle *)standardError libraryBookmark:(NSData *)libBookmark argv:(NSArray<NSString *> *)argv completion:(void(^)(BOOL,NSString *))completion;
 - (void)terminate;
 
+/// Helper holds on to the token to keep this XPC service active
+///
+/// If this is not called after `startQemu`, XNU may terminate this helper as idle.
+/// - Parameter token: Token to hold, result may be discarded.
+- (void)assertActiveWithToken:(tokenCallback_t)token;
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 4 - 0
Services/UTMProcess.m

@@ -234,6 +234,10 @@ static int defaultEntry(UTMProcess *self, int argc, const char *argv[], const ch
     NSFileHandle *standardError = self.standardError.fileHandleForWriting;
     [_connection.remoteObjectProxy setEnvironment:self.environment];
     [_connection.remoteObjectProxy setCurrentDirectoryPath:self.currentDirectoryUrl.path];
+    // this is needed to prevent XNU from terminating an idle XPC helper
+    [_connection.remoteObjectProxy assertActiveWithToken:^(BOOL ignored) {
+        // do nothing
+    }];
     [[_connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
         if (error.domain == NSCocoaErrorDomain && error.code == NSXPCConnectionInvalid) {
             // inhibit this error since we always see it on quit