Skip to content

Commit 529a4d2

Browse files
authored
Disable sandboxing for macOS apps and tests in CI (#149618)
macOS 14 added new requirements that un-codesigned sandbox apps must be granted access when changed. Waiting for this UI caused macOS tests to fail on macOS 14 because the test runner forced codesigning off. Additionally, adding codesigning is not sufficient, since it must still be approved before codesigning is enough to pass the check. As a workaround, this PR disables sandboxing for macOS apps/tests in CI. ![Screenshot 2024-05-30 at 2 41 33�PM](https://github.com/flutter/flutter/assets/682784/1bc32620-5edb-420a-866c-5cc529b2ac55) https://developer.apple.com/documentation/updates/security#June-2023) > App Sandbox now associates your macOS app with its sandbox container using its code signature. The operating system asks the person using your app to grant permission if it tries to access a sandbox container associated with a different app. For more information, see [Accessing files from the macOS App Sandbox](https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox). And that link explains why this is happening on a macOS 14 update: > In macOS 14 and later, the operating system uses your app�s code signature to associate it with its sandbox container. If your app tries to access the sandbox container owned by another app, the system asks the person using your app whether to grant access. If the person denies access and your app is already running, then it can�t read or write the files in the other app�s sandbox container. If the person denies access while your app is launching and trying to enter the other app�s sandbox container, your app fails to launch. > > The operating system also tracks the association between an app�s code signing identity and its sandbox container for helper tools, including launch agents. If a person denies permission for a launch agent to enter its sandbox container and the app fails to start, launchd starts the launch agent again and the operating system re-requests access. Fixes flutter/flutter#149268. Fixes framework part of flutter/flutter#149264. Might fix packages issue: flutter/flutter#149329. Verified framework tests: https://ci.chromium.org/ui/p/flutter/builders/staging.shadow/Mac%20plugin_test_macos/9/overview https://ci.chromium.org/ui/p/flutter/builders/staging.shadow/Mac%20run_debug_test_macos/2/overview https://ci.chromium.org/ui/p/flutter/builders/staging.shadow/Mac%20tool_integration_tests_4_4/2/overview https://ci.chromium.org/ui/p/flutter/builders/staging.shadow/Mac%20integration_ui_test_test_macos/3/overview https://ci.chromium.org/ui/p/flutter/builders/staging.shadow/Mac%20flavors_test_macos/3/overview https://ci.chromium.org/ui/p/flutter/builders/staging.shadow/Mac_benchmark%20complex_layout_scroll_perf_macos__timeline_summary/6/overview
1 parent f24bd99 commit 529a4d2

File tree

12 files changed

+405
-3
lines changed

12 files changed

+405
-3
lines changed

dev/devicelab/lib/framework/ios.dart

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,13 @@ Future<bool> runXcodeTests({
191191
codeSignStyle = environment['FLUTTER_XCODE_CODE_SIGN_STYLE'];
192192
provisioningProfile = environment['FLUTTER_XCODE_PROVISIONING_PROFILE_SPECIFIER'];
193193
}
194+
File? disabledSandboxEntitlementFile;
195+
if (platformDirectory.endsWith('macos')) {
196+
disabledSandboxEntitlementFile = _createDisabledSandboxEntitlementFile(
197+
platformDirectory,
198+
configuration,
199+
);
200+
}
194201
final String resultBundleTemp = Directory.systemTemp.createTempSync('flutter_xcresult.').path;
195202
final String resultBundlePath = path.join(resultBundleTemp, 'result');
196203
final int testResultExit = await exec(
@@ -214,6 +221,8 @@ Future<bool> runXcodeTests({
214221
'CODE_SIGN_STYLE=$codeSignStyle',
215222
if (provisioningProfile != null)
216223
'PROVISIONING_PROFILE_SPECIFIER=$provisioningProfile',
224+
if (disabledSandboxEntitlementFile != null)
225+
'CODE_SIGN_ENTITLEMENTS=${disabledSandboxEntitlementFile.path}',
217226
],
218227
workingDirectory: platformDirectory,
219228
canFail: true,
@@ -247,3 +256,55 @@ Future<bool> runXcodeTests({
247256
}
248257
return true;
249258
}
259+
260+
/// Finds and copies macOS entitlements file. In the copy, disables sandboxing.
261+
/// If entitlements file is not found, returns null.
262+
///
263+
/// As of macOS 14, testing a macOS sandbox app may prompt the user to grant
264+
/// access to the app. To workaround this in CI, we create and use a entitlements
265+
/// file with sandboxing disabled. See
266+
/// https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox.
267+
File? _createDisabledSandboxEntitlementFile(
268+
String platformDirectory,
269+
String configuration,
270+
) {
271+
String entitlementDefaultFileName;
272+
if (configuration == 'Release') {
273+
entitlementDefaultFileName = 'Release';
274+
} else {
275+
entitlementDefaultFileName = 'DebugProfile';
276+
}
277+
278+
final String entitlementFilePath = path.join(
279+
platformDirectory,
280+
'Runner',
281+
'$entitlementDefaultFileName.entitlements',
282+
);
283+
final File entitlementFile = File(entitlementFilePath);
284+
285+
if (!entitlementFile.existsSync()) {
286+
print('Unable to find entitlements file at ${entitlementFile.path}');
287+
return null;
288+
}
289+
290+
final String originalEntitlementFileContents =
291+
entitlementFile.readAsStringSync();
292+
final String tempEntitlementPath = Directory.systemTemp
293+
.createTempSync('flutter_disable_sandbox_entitlement.')
294+
.path;
295+
final File disabledSandboxEntitlementFile = File(path.join(
296+
tempEntitlementPath,
297+
'${entitlementDefaultFileName}WithDisabledSandboxing.entitlements',
298+
));
299+
disabledSandboxEntitlementFile.createSync(recursive: true);
300+
disabledSandboxEntitlementFile.writeAsStringSync(
301+
originalEntitlementFileContents.replaceAll(
302+
RegExp(r'<key>com\.apple\.security\.app-sandbox<\/key>[\S\s]*?<true\/>'),
303+
'''
304+
<key>com.apple.security.app-sandbox</key>
305+
<false/>''',
306+
),
307+
);
308+
309+
return disabledSandboxEntitlementFile;
310+
}

packages/flutter_tools/lib/src/commands/build_macos.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class BuildMacosCommand extends BuildSubCommand {
7272
flutterUsage: globals.flutterUsage,
7373
analytics: analytics,
7474
),
75+
usingCISystem: usingCISystem,
7576
);
7677
return FlutterCommandResult.success();
7778
}

packages/flutter_tools/lib/src/desktop_device.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import 'convert.dart';
1616
import 'devfs.dart';
1717
import 'device.dart';
1818
import 'device_port_forwarder.dart';
19+
import 'globals.dart' as globals;
20+
import 'macos/macos_device.dart';
1921
import 'protocol_discovery.dart';
2022

2123
/// A partial implementation of Device for desktop-class devices to inherit
@@ -119,6 +121,7 @@ abstract class DesktopDevice extends Device {
119121
await buildForDevice(
120122
buildInfo: debuggingOptions.buildInfo,
121123
mainPath: mainPath,
124+
usingCISystem: debuggingOptions.usingCISystem,
122125
);
123126
}
124127

@@ -159,8 +162,39 @@ abstract class DesktopDevice extends Device {
159162
logger: _logger,
160163
);
161164
try {
165+
Timer? timer;
166+
if (this is MacOSDevice) {
167+
if (await globals.isRunningOnBot) {
168+
const int defaultTimeout = 5;
169+
timer = Timer(const Duration(minutes: defaultTimeout), () {
170+
// As of macOS 14, if sandboxing is enabled and the app is not codesigned,
171+
// a dialog will prompt the user to allow the app to run. This will
172+
// cause tests in CI to hang. In CI, we workaround this by setting
173+
// the CODE_SIGN_ENTITLEMENTS build setting to a version with
174+
// sandboxing disabled.
175+
final String sandboxingMessage;
176+
if (debuggingOptions.usingCISystem) {
177+
sandboxingMessage = 'Ensure sandboxing is disabled by checking '
178+
'the set CODE_SIGN_ENTITLEMENTS.';
179+
} else {
180+
sandboxingMessage = 'Consider codesigning your app or disabling '
181+
'sandboxing. Flutter will attempt to disable sandboxing if '
182+
'the `--ci` flag is provided.';
183+
}
184+
_logger.printError(
185+
'The Dart VM Service was not discovered after $defaultTimeout '
186+
'minutes. If the app has sandboxing enabled and is not '
187+
'codesigned or codesigning changed, this may be caused by a '
188+
'system prompt asking for access. $sandboxingMessage\n'
189+
'See https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox '
190+
'for more information.');
191+
});
192+
}
193+
}
194+
162195
final Uri? vmServiceUri = await vmServiceDiscovery.uri;
163196
if (vmServiceUri != null) {
197+
timer?.cancel();
164198
onAttached(package, buildInfo, process);
165199
return LaunchResult.succeeded(vmServiceUri: vmServiceUri);
166200
}
@@ -199,6 +233,7 @@ abstract class DesktopDevice extends Device {
199233
Future<void> buildForDevice({
200234
required BuildInfo buildInfo,
201235
String? mainPath,
236+
bool usingCISystem = false,
202237
});
203238

204239
/// Returns the path to the executable to run for [package] on this device for

packages/flutter_tools/lib/src/linux/linux_device.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class LinuxDevice extends DesktopDevice {
6262
Future<void> buildForDevice({
6363
String? mainPath,
6464
required BuildInfo buildInfo,
65+
bool usingCISystem = false,
6566
}) async {
6667
await buildLinux(
6768
FlutterProject.current().linux,

packages/flutter_tools/lib/src/macos/build_macos.dart

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Future<void> buildMacOS({
6565
required bool verboseLogging,
6666
bool configOnly = false,
6767
SizeAnalyzer? sizeAnalyzer,
68+
bool usingCISystem = false,
6869
}) async {
6970
final Directory? xcodeWorkspace = flutterProject.macos.xcodeWorkspace;
7071
if (xcodeWorkspace == null) {
@@ -153,6 +154,19 @@ Future<void> buildMacOS({
153154
'Building macOS application...',
154155
);
155156
int result;
157+
158+
File? disabledSandboxEntitlementFile;
159+
if (usingCISystem) {
160+
disabledSandboxEntitlementFile = _createDisabledSandboxEntitlementFile(
161+
flutterProject.macos,
162+
configuration,
163+
);
164+
if (disabledSandboxEntitlementFile != null) {
165+
globals.logger.printStatus(
166+
'Detected macOS app running in CI, turning off sandboxing.');
167+
}
168+
}
169+
156170
try {
157171
result = await globals.processUtils.stream(<String>[
158172
'/usr/bin/env',
@@ -170,6 +184,8 @@ Future<void> buildMacOS({
170184
else
171185
'-quiet',
172186
'COMPILER_INDEX_STORE_ENABLE=NO',
187+
if (disabledSandboxEntitlementFile != null)
188+
'CODE_SIGN_ENTITLEMENTS=${disabledSandboxEntitlementFile.path}',
173189
...environmentVariablesAsXcodeBuildSettings(globals.platform),
174190
],
175191
trace: true,
@@ -271,3 +287,52 @@ Future<void> _writeCodeSizeAnalysis(BuildInfo buildInfo, SizeAnalyzer? sizeAnaly
271287
'dart devtools --appSizeBase=$relativeAppSizePath'
272288
);
273289
}
290+
291+
/// Finds and copies macOS entitlements file. In the copy, disables sandboxing.
292+
/// If entitlements file is not found, returns null.
293+
///
294+
/// As of macOS 14, running a macOS sandbox app may prompt the user to grant
295+
/// access to the app. To workaround this in CI, we create and use a entitlements
296+
/// file with sandboxing disabled. See
297+
/// https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox.
298+
File? _createDisabledSandboxEntitlementFile(
299+
MacOSProject macos,
300+
String configuration,
301+
) {
302+
String entitlementDefaultFileName;
303+
if (configuration == 'Release') {
304+
entitlementDefaultFileName = 'Release';
305+
} else {
306+
entitlementDefaultFileName = 'DebugProfile';
307+
}
308+
309+
// TODO(vashworth): Once https://github.com/flutter/flutter/issues/146204 is
310+
// fixed, it would be better to get the path to the entitlement file from the
311+
// project's build settings (CODE_SIGN_ENTITLEMENTS).
312+
final File entitlementFile = macos.hostAppRoot
313+
.childDirectory('Runner')
314+
.childFile('$entitlementDefaultFileName.entitlements');
315+
316+
if (!entitlementFile.existsSync()) {
317+
globals.logger.printTrace(
318+
'Unable to find entitlements file at ${entitlementFile.path}');
319+
return null;
320+
}
321+
322+
final String entitlementFileContents = entitlementFile.readAsStringSync();
323+
final File disabledSandboxEntitlementFile = globals.fs.systemTempDirectory
324+
.createTempSync('flutter_disable_sandbox_entitlement.')
325+
.childFile(
326+
'${entitlementDefaultFileName}WithDisabledSandboxing.entitlements',
327+
);
328+
disabledSandboxEntitlementFile.createSync(recursive: true);
329+
disabledSandboxEntitlementFile.writeAsStringSync(
330+
entitlementFileContents.replaceAll(
331+
RegExp(r'<key>com\.apple\.security\.app-sandbox<\/key>[\S\s]*?<true\/>'),
332+
'''
333+
<key>com.apple.security.app-sandbox</key>
334+
<false/>''',
335+
),
336+
);
337+
return disabledSandboxEntitlementFile;
338+
}

packages/flutter_tools/lib/src/macos/macos_device.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,14 @@ class MacOSDevice extends DesktopDevice {
7070
Future<void> buildForDevice({
7171
required BuildInfo buildInfo,
7272
String? mainPath,
73+
bool usingCISystem = false,
7374
}) async {
7475
await buildMacOS(
7576
flutterProject: FlutterProject.current(),
7677
buildInfo: buildInfo,
7778
targetOverride: mainPath,
7879
verboseLogging: _logger.isVerbose,
80+
usingCISystem: usingCISystem,
7981
);
8082
}
8183

packages/flutter_tools/lib/src/macos/macos_ipad_device.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class MacOSDesignedForIPadDevice extends DesktopDevice {
116116
Future<void> buildForDevice({
117117
String? mainPath,
118118
required BuildInfo buildInfo,
119+
bool usingCISystem = false,
119120
}) async {
120121
// Only attaching to a running app launched from Xcode is supported.
121122
throw UnimplementedError('Building for "$name" is not supported.');

packages/flutter_tools/lib/src/runner/flutter_command.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,16 @@ abstract class FlutterCommand extends Command<void> {
376376
String? get packagesPath => stringArg(FlutterGlobalOptions.kPackagesOption, global: true);
377377

378378
/// Whether flutter is being run from our CI.
379-
bool get usingCISystem => boolArg(FlutterGlobalOptions.kContinuousIntegrationFlag, global: true);
379+
///
380+
/// This is true if `--ci` is passed to the command or if environment
381+
/// variable `LUCI_CI` is `True`.
382+
bool get usingCISystem {
383+
return boolArg(
384+
FlutterGlobalOptions.kContinuousIntegrationFlag,
385+
global: true,
386+
) ||
387+
globals.platform.environment['LUCI_CI'] == 'True';
388+
}
380389

381390
String? get debugLogsDirectoryPath => stringArg(FlutterGlobalOptions.kDebugLogsDirectoryFlag, global: true);
382391

packages/flutter_tools/lib/src/windows/windows_device.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class WindowsDevice extends DesktopDevice {
6060
Future<void> buildForDevice({
6161
String? mainPath,
6262
required BuildInfo buildInfo,
63+
bool usingCISystem = false,
6364
}) async {
6465
await buildWindows(
6566
FlutterProject.current().windows,

0 commit comments

Comments
 (0)