Skip to content

Commit 071a5d1

Browse files
committed
[tools] Add a tool to launch processes disclaming responsibility.
TODO: improve description.
1 parent 6aa3e82 commit 071a5d1

File tree

8 files changed

+188
-1
lines changed

8 files changed

+188
-1
lines changed

tests/xharness/Harness.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,29 @@ string GetVariable (string variable, string @default = null)
194194
return result;
195195
}
196196

197+
#nullable enable
198+
string? spawnerPath;
199+
public string SpawnerPath {
200+
get {
201+
if (spawnerPath is null)
202+
spawnerPath = Path.GetFullPath (Path.Combine (RootDirectory, "..", "tools", "spawner", "spawner"));
203+
return spawnerPath;
204+
}
205+
}
206+
207+
public void UseSpawner (ProcessStartInfo processStartInfo, IList<string> arguments)
208+
{
209+
if (!string.IsNullOrEmpty (processStartInfo.Arguments))
210+
throw new InvalidOperationException ($"ProcessStartInfo.Arguments must be empty when using UseSpawner.");
211+
212+
var originalFileName = processStartInfo.FileName;
213+
processStartInfo.FileName = SpawnerPath;
214+
processStartInfo.ArgumentList.Add (originalFileName);
215+
foreach (var args in arguments)
216+
processStartInfo.ArgumentList.Add (args);
217+
}
218+
#nullable disable
219+
197220
public List<TestProject> TestProjects { get; } = new ();
198221

199222
readonly bool useSystemXamarinIOSMac; // if the system XI/XM should be used, or the locally build XI/XM.

tests/xharness/IHarness.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public interface IHarness {
4646
bool InCI { get; }
4747
bool UseTcpTunnel { get; }
4848
string VSDropsUri { get; }
49+
string SpawnerPath { get; }
50+
void UseSpawner (System.Diagnostics.ProcessStartInfo startInfo, IList<string> arguments);
4951

5052
#endregion
5153

tests/xharness/Jenkins/TestTasks/MacExecuteTask.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@ public override async Task RunTestAsync ()
8585
proc.StartInfo.EnvironmentVariables ["DISABLE_SYSTEM_PERMISSION_TESTS"] = "1";
8686
proc.StartInfo.EnvironmentVariables ["MONO_DEBUG"] = "no-gdb-backtrace";
8787
proc.StartInfo.EnvironmentVariables.Remove ("DYLD_FALLBACK_LIBRARY_PATH"); // VSMac might set this, and the test may end up crashing
88-
proc.StartInfo.Arguments = StringUtils.FormatArguments (arguments);
88+
89+
// Use the spawner to launch the app, to avoid issues with macOS getting confused who's the responsible process
90+
Harness.UseSpawner (proc.StartInfo, arguments);
91+
8992
Jenkins.MainLog.WriteLine ("Executing {0} ({1})", TestName, Mode);
9093
var log = Logs.Create ($"execute-{Platform}-{Timestamp}.txt", LogType.ExecutionLog.ToString ());
9194
ICrashSnapshotReporter snapshot = null;

tools/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ endif
2525
SUBDIRS+=mlaunch
2626

2727
SUBDIRS += dotnet-linker
28+
SUBDIRS += spawner

tools/spawner/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.libs
2+
spawner
3+

tools/spawner/Makefile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
TOP=../..
2+
include $(TOP)/Make.config
3+
include $(TOP)/mk/rules.mk
4+
5+
.libs/osx-arm64/spawner: .libs/osx-arm64/spawner.o
6+
$(Q_CCLD) $(CLANG) -arch $(DOTNET_osx-arm64_ARCHITECTURES) $< -o$@ -isysroot $(macos_SDK)
7+
8+
.libs/osx-x64/spawner: .libs/osx-x64/spawner.o
9+
$(Q_CCLD) $(CLANG) -arch $(DOTNET_osx-x64_ARCHITECTURES) $< -o$@ -isysroot $(macos_SDK)
10+
11+
spawner: .libs/osx-arm64/spawner .libs/osx-x64/spawner
12+
$(Q_LIPO) $(LIPO) $^ -create -output $@
13+
$(Q) chmod +x $@
14+
15+
all-local:: spawner

tools/spawner/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
SPAWNER
2+
=======
3+
4+
This is a very simple tool, which executes another process, disclaiming any
5+
responsibility for it.
6+
7+
This is important when executing tests apps, because when macOS sees that an
8+
app uses API that needs specific entries in the Info.plist (such as the
9+
`NSAppleMusicUsageDescription`), the responsible process is where macOS looks
10+
for said key.
11+
12+
Example crash report:
13+
14+
```
15+
Process: introspection [85822]
16+
Path: /Users/USER/*/introspection.app/Contents/MacOS/introspection
17+
Identifier: com.xamarin.introspection
18+
Version: 1.0 (1.0)
19+
Code Type: ARM-64 (Native)
20+
Parent Process: dotnet [81129]
21+
Responsible: Electron [68966]
22+
User ID: 501
23+
24+
Date/Time: 2025-11-13 17:33:10.8123 +0100
25+
OS Version: macOS 15.7.2 (24G325)
26+
Report Version: 12
27+
Anonymous UUID: F22C0F06-0F16-E475-C0CB-264A0FF4F6A3
28+
29+
30+
Time Awake Since Boot: 27000 seconds
31+
32+
System Integrity Protection: enabled
33+
34+
Crashed Thread: 16 Dispatch queue: com.apple.root.default-qos
35+
36+
Exception Type: EXC_CRASH (SIGKILL)
37+
Exception Codes: 0x0000000000000000, 0x0000000000000000
38+
39+
Termination Reason: Namespace TCC, Code 0
40+
This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSAppleMusicUsageDescription key with a string value explaining to the user how the app uses this data.
41+
```
42+
43+
The app crashed because macOS says it needs the `NSAppleMusicUsageDescription` entry in its `Info.plist` file.
44+
45+
This is confusing, because introspection has an `NSAppleMusicUsageDescription` entry in its `Info.plist` file.
46+
47+
Here's what happens:
48+
49+
Note that there's a "Responsible [Process]" (Electron 68966) line, which is not the same as "Process" (introspection 85822), and this is the crux of the matter.
50+
51+
In this particular case:
52+
53+
* I opened the xharness project in VSCode.
54+
* I launched the xharness project in the debugger, and then ran introspection for Mac Catalyst.
55+
* The responsible process ended up being VS Code (aka Electron, with pid 85822), and that's where macOS ended up looking for the `NSAppleMusicUsageDescription` key.
56+
57+
The fix is to launch `introspection` (and any other test app on macOS) using
58+
this `spawner` tool, which disclaims reponsibility for anything it launches,
59+
thus letting `introspection` be a grown up process and fully responsible for
60+
itself.
61+
62+
Usage is simple: just pass the executable + any arguments to `spawner`.
63+
64+
References:
65+
66+
* https://gitlab.com/gnachman/iterm2/-/issues/10360
67+
* https://github.com/llvm/llvm-project/commit/041c7b84a4b925476d1e21ed302786033bb6035f#diff-a38ae411ccf0c85f3d7c0c45d8e1ad035030d5171d59e478b86a094941d3209dR16-R17
68+
* https://lldb.llvm.org/cpp_reference/PosixSpawnResponsible_8h_source.html
69+
* https://steipete.me/posts/2025/applescript-cli-macos-complete-guide

tools/spawner/spawner.c

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
#include <dispatch/dispatch.h>
5+
#include <dlfcn.h>
6+
#include <signal.h>
7+
#include <spawn.h>
8+
#include <stdio.h>
9+
10+
errno_t responsibility_spawnattrs_setdisclaim (posix_spawnattr_t *attrs, bool disclaim);
11+
12+
int main (int argc, char** argv, char** envp)
13+
{
14+
if (argc < 2) {
15+
fprintf (stderr,
16+
"spawner: launch a subprocess, disclaiming all responsibilities with regards to TCC:\n"
17+
"usage: spawner <command> [arguments]\n");
18+
return 1;
19+
}
20+
21+
int rv;
22+
// Behave as exec
23+
short flags = POSIX_SPAWN_SETEXEC;
24+
posix_spawnattr_t spawnattr;
25+
sigset_t sigset;
26+
27+
rv = posix_spawnattr_init (&spawnattr);
28+
if (rv) {
29+
fprintf (stderr, "Failed to execute 'posix_spawnattr_init': %i (%s)\n", rv, strerror (rv));
30+
return 1;
31+
}
32+
33+
// Reset the signal mask
34+
sigemptyset (&sigset);
35+
rv = posix_spawnattr_setsigmask (&spawnattr, &sigset);
36+
if (rv) {
37+
fprintf (stderr, "Failed to execute 'posix_spawnattr_setsigmask': %i (%s)\n", rv, strerror (rv));
38+
return 1;
39+
}
40+
flags |= POSIX_SPAWN_SETSIGMASK;
41+
42+
// Reset all signals to their default handlers
43+
sigfillset (&sigset);
44+
rv = posix_spawnattr_setsigdefault (&spawnattr, &sigset);
45+
if (rv) {
46+
fprintf (stderr, "Failed to execute 'posix_spawnattr_setsigdefault': %i (%s)\n", rv, strerror (rv));
47+
return 1;
48+
}
49+
flags |= POSIX_SPAWN_SETSIGDEF;
50+
51+
rv = posix_spawnattr_setflags (&spawnattr, flags);
52+
if (rv) {
53+
fprintf (stderr, "Failed to execute 'posix_spawnattr_setflags': %i (%s)\n", rv, strerror (rv));
54+
return 1;
55+
}
56+
57+
rv = responsibility_spawnattrs_setdisclaim (&spawnattr, 1);
58+
if (rv) {
59+
fprintf (stderr, "Failed to execute 'responsibility_spawnattrs_setdisclaim': %i (%s)\n", rv, strerror (rv));
60+
return 1;
61+
}
62+
63+
pid_t pid = 0;
64+
rv = posix_spawnp (&pid, argv [1], NULL, &spawnattr, argv + 1, envp);
65+
posix_spawnattr_destroy (&spawnattr);
66+
67+
// posix_spawnp shouldn't return (because we set the POSIX_SPAWN_SETEXEC flag)
68+
// so if it did, something went wrong
69+
fprintf (stderr, "Failed to execute '%s': %i (%s)\n", argv [1], rv, strerror (rv));
70+
return 1;
71+
}

0 commit comments

Comments
 (0)