Privacy for XPC module

I have an application which is doing screen recording, now I move the screen recording feature to a standalone native XPC module for better performance due to some reason that the app is tied an old lib which cannot generate native code for M1 (Intel only).

My question is that, this new xpc module is belong to the App (demanded by the app), if I give the screen recording permission to the app, will the xpc screen scraping module be granted to the permission?

Right now looks like it is not after I granted the application with the screen recording permission since display stream won't produce the frame data.

“XPC module” isn’t a term we use. I suspect that you’re talking about an XPC Service, as defined in the xpcservice.plist man page.

If so, it should share the TCC privileges of the container app. I’ve been testing this a bunch recently and my experience is that this works well for XPC Services [1]. I just fired up my testbed and confirmed that. Unfortunately my testbed doesn’t support screen recording, so I tested the CGEventTap instead. However, my experience in other situations is that CGDisplayStream and CGEventTap have very similar behaviour when it comes to TCC.

There’s a few potential gotchas here:

  • Test on a fresh machine. It’s not uncommon for TCC get confused on your development machine.

    I usually use a VM for this sort of thing, restoring from a snapshot between tests.

  • Make sure you’re testing on the latest macOS (I’m using 12.3.1). This stuff changes quite regularly.

  • When you enable System Preferences > Security & Privacy > Privacy > Screen Recording for an app, the system asks whether you want to quit and relaunch it. Similarly, you need to make sure your XPC Service terminates when you change the setting.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] It doesn’t work for other related types of code, like Service Management login items.

Thanks Quinn, I believe what you said is right, could be my env issue, I have an App, due to it is using the third-party lib which doesn't support M1, I turn one of its feature into XPC service for performance, I managed to get the app built on M1 on Monterey 12.4(It has to use Xcode 11.7 to build the App, I build the XPC service with Xcode 13.4 for universal binary, this XPC service trigged by the App when using, dismissed by the App when stop using), on M1, the unsigned debug version cannot pass the screen record permission to the XPC service(Intel Mac doesn't have this issue), This caused that I cannot debug the code under M1 Mac, but release, signed and notarized client and the XPC service are working.

BTW, I'm facing another XPC service issue, I'm trying to scrape Window in the XPC services, somehow I cannot get image with the CGWindowID passed from host app, then I try to enumerate the WindowID to find the one I want to scrape, anyway both CGWindowListCopyWindowInfo and CGWindowListCreate are always returning null (in Intel Mac)

  //Find WindowID of the process.
  auto findWindowId = [](uint32_t pid) -> CGWindowID
  {
    CFArrayRef dict = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
    //will return 0x0
    SAFE_CFRELEASE(dict);
    dict = CGWindowListCreate(kCGWindowListOptionAll, kCGNullWindowID);
    //will return 0x0, this is just for testing
    SAFE_CFRELEASE(dict);
     
    return kCGNullWindowID;
  };
  auto windowId = findWindowId(GetSourceID());

Can we call CGWindowListCreate and CGWindowListCopyWindowInfo in XPC service? The host app has the screen capture permission.

-Steven

is XPC service outside of a GUI security session?

An XPC service that’s embedded in your app runs in the same GUI login session as your app.

both CGWindowListCopyWindowInfo and CGWindowListCreate are always returning null

Weird. I’ve no immediate explanation for that. It’s not a TCC issue because TCC allows these calls to go through but sets things up so that they don’t return any private info.

the unsigned debug version cannot pass the screen record permission to the XPC service

Unsigned? Don’t work with unsigned code, especially when dealing with TCC, because TCC uses information from your code signature to track authorisation. This is something I discuss in depth in TN3127 Inside Code Signing: Requirements.

Rather, during day-to-day development sign all of your code using an Apple Development signing identity.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I used to pass the CGWindowID from host to the XPC service, and both CGWindowListCreateImage and CGWindowListCreateImageFromArray won't grab any image (null returned), later I pass the process pid from the host to the XPC, then use

auto findWindowIds = [](uint32_t pId) -> CFArrayRef
  {
    auto appRef = AXUIElementCreateApplication(pId);
    CFMutableArrayRef idArray = CFArrayCreateMutable(0, 0, nullptr);
    CFIndex count = 0;
    CFArrayRef windowArray = NULL;
    auto err = AXUIElementGetAttributeValueCount(appRef, CFSTR("AXWindows"), &count);
    if (err == kAXErrorSuccess && count)
    {
      AXUIElementCopyAttributeValues(appRef, CFSTR("AXWindows"), 0, count, &windowArray);
      for (int idx = 0; idx < count; idx ++)
      {
        AXUIElementRef element = (AXUIElementRef)
                  CFArrayGetValueAtIndex(windowArray, idx);
        CGWindowID temp = 0;
        _AXUIElementGetWindow(element, &temp);
        LOGEX("windowId: %u", temp);
        CFArrayAppendValue(idArray, reinterpret_cast<void*>(temp));
      }
    }
    SAFE_CFRELEASE(appRef);
    return idArray;
  };

It does get the correct windowId, anyway, CGWindowListCreateImage and CGWindowListCreateImageFromArray still cannot grab any image.

Passing window IDs between processes is not supported.

As to why your XPC service is unable to get a snapshot of the window’s contents, I would expect that to work as long as the container app has been granted the Screen Recording privilege by the user. If you call CGPreflightScreenCaptureAccess in both the container app and the XPC service, what do you get back?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I'm using the process id now, the XPC can enumerate the WindowID with the AXUI API.

In the XPC, before dealing with the window scraping, I did check screencapture and it returns positive.

auto capture_able = CGPreflightScreenCaptureAccess();
  if ( !capture_able)
  {
    LOGEX("This process doesn't have screen capture granted!");
    return;
  }
 //XPC side check it is possitive.

And this XPC doesn't have problem in Scraping display with cgdisplaystream, only has problem with Quartz Window Service APIs, none of them behavior as expected.

Thanks Steven

the XPC can enumerate the WindowID with the AXUI API.

To be clear, the routine you mentioned above, _AXUIElementGetWindow, is not API.

And this XPC doesn't have problem in Scraping display with cgdisplaystream, only has problem with Quartz Window Service APIs

Presumably “Quartz Window Service APIs” means CGWindowListCreate and friends. And the initial sign of problems is that CGWindowListCopyWindowInfo is returning NULL?

The values returned by CGWindowListCopyWindowInfo are modified by the Screen Recording privilege but the absence of that privilege does not cause the routine to fail. Rather, it just means you won’t get back potentially private values like kCGWindowName. If the entire API is failing, that’s some other issue.

I suspect that this is failing because your XPC service hasn’t initialised enough of AppKit to connect up to the window server. What does the main function of your XPC service look like?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks, hope you had a great long weekend, yes, entire Quartz Window Services APIs are failing, but Quartz Display APIs are working, my XPC is pretty simple:

int main(int argc, const char* argv[])
{
  #pragma unused(argc, argv)
  LOGEX("start Service");
  xpc_main( new_connection_handler );
  exit(EXIT_FAILURE);
}
...
void xpcEventHandler(const xpc_connection_t conn, const xpc_object_t event)
{
      xpc_type_t type = xpc_get_type(event);
      if (type == XPC_TYPE_DICTIONARY) {
         std::async(std::launch::async, handeClientRequest, conn, event);
      } else {
    if (event == XPC_ERROR_CONNECTION_INVALID) {
      LOGEX("xpcEventHandler peer has closed the connection!!");
    } else {
      LOGEX("xpcEventHandler service will be terminated soon!!");
    }
  }

}
...
void new_connection_handler(const xpc_connection_t conn)
{
  xpc_connection_set_event_handler(conn, ^(xpc_object_t event) {
    xpcEventHandler(conn, event);
  });

  xpc_connection_resume((xpc_connection_t)xpc_retain(conn));
}
...

If it is not by design, what you guess AppKit didn't instantiate the Window Server could the reason, Is there any API for instantiate ?

BTW, if Quartz Windows API call failed, is there something like Windows GetLastError which provide the failure cause?

Thanks Steven

if Quartz Windows API call failed, is there something like Windows GetLastError which provide the failure cause?

No. Honestly, these routines should never fail, as long as you run them in the expected environment.

You may be able to find some information about the failure in the system log. For info on that, see Your Friend the System Log.

my XPC is pretty simple

OK.

Is there any API for instantiate?

No. Normally you do this by having main call NSApplicationMain.

At this point I’ve reached the limit of what I’m comfortable talking about on DevForums. I need to do more in-depth research to get you an answer. I recommend that you open a DTS tech support incident so that I can allocate the time for that research. Make sure you reference this thread so that your incident comes to me.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Created  801108294

Thanks a lot!

-Steven

I thought I’d post the result here, for the benefit of all…

The issue here was specific to low-level CG APIs like CGWindowListCopyWindowInfo. High-level APIs, like NSWindow, worked fine in the XPC service. These low-level APIs use a different path to connect to the window server and that path fails from the XPC service because it’s running in a different session. That’s why CGWindowListCopyWindowInfo returned nil.

The fix is to put the XPC service in the same session as the app by adding the JoinExistingSession property to its property list. For details on that property, see the xpcservice.plist man page.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Privacy for XPC module
 
 
Q