diff --git a/src/node.cc b/src/node.cc
index 3895b2b4076e0c..b3b69922cbe8ab 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -737,7 +737,10 @@ void ResetStdio() {
err = tcsetattr(fd, TCSANOW, &s.termios);
while (err == -1 && errno == EINTR); // NOLINT
CHECK_EQ(0, pthread_sigmask(SIG_UNBLOCK, &sa, nullptr));
- CHECK_EQ(0, err);
+
+ // Normally we expect err == 0. But if macOS App Sandbox is enabled,
+ // tcsetattr will fail with err == -1 and errno == EPERM.
+ CHECK_IMPLIES(err != 0, err == -1 && errno == EPERM);
}
}
#endif // __POSIX__
diff --git a/test/fixtures/macos-app-sandbox/Info.plist b/test/fixtures/macos-app-sandbox/Info.plist
new file mode 100644
index 00000000000000..38362085af4bf8
--- /dev/null
+++ b/test/fixtures/macos-app-sandbox/Info.plist
@@ -0,0 +1,24 @@
+
+
+
+
+ CFBundleExecutable
+ node
+ CFBundleIdentifier
+ org.nodejs.test.node_sandboxed
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ node_sandboxed
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSupportedPlatforms
+
+ MacOSX
+
+ CFBundleVersion
+ 1
+
+
\ No newline at end of file
diff --git a/test/fixtures/macos-app-sandbox/node_sandboxed.entitlements b/test/fixtures/macos-app-sandbox/node_sandboxed.entitlements
new file mode 100644
index 00000000000000..852fa1a4728ae4
--- /dev/null
+++ b/test/fixtures/macos-app-sandbox/node_sandboxed.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+
diff --git a/test/parallel/test-macos-app-sandbox.js b/test/parallel/test-macos-app-sandbox.js
new file mode 100644
index 00000000000000..f7fcf5e5728815
--- /dev/null
+++ b/test/parallel/test-macos-app-sandbox.js
@@ -0,0 +1,65 @@
+'use strict';
+const common = require('../common');
+if (process.platform !== 'darwin')
+ common.skip('App Sandbox is only avaliable on Darwin');
+
+const fixtures = require('../common/fixtures');
+const tmpdir = require('../common/tmpdir');
+const assert = require('assert');
+const child_process = require('child_process');
+const path = require('path');
+const fs = require('fs');
+const os = require('os');
+
+const nodeBinary = process.execPath;
+
+tmpdir.refresh();
+
+const appBundlePath = path.join(tmpdir.path, 'node_sandboxed.app');
+const appBundleContentPath = path.join(appBundlePath, 'Contents');
+const appExecutablePath = path.join(
+ appBundleContentPath, 'MacOS', 'node');
+
+// Construct the app bundle and put the node executable in it:
+// node_sandboxed.app/
+// └── Contents
+// ├── Info.plist
+// ├── MacOS
+// │ └── node
+fs.mkdirSync(appBundlePath);
+fs.mkdirSync(appBundleContentPath);
+fs.mkdirSync(path.join(appBundleContentPath, 'MacOS'));
+fs.copyFileSync(
+ fixtures.path('macos-app-sandbox', 'Info.plist'),
+ path.join(appBundleContentPath, 'Info.plist'));
+fs.copyFileSync(
+ nodeBinary,
+ appExecutablePath);
+
+
+// Sign the app bundle with sandbox entitlements:
+assert.strictEqual(
+ child_process.spawnSync('/usr/bin/codesign', [
+ '--entitlements', fixtures.path(
+ 'macos-app-sandbox', 'node_sandboxed.entitlements'),
+ '-s', '-',
+ appBundlePath
+ ]).status,
+ 0);
+
+// Sandboxed app shouldn't be able to read the home dir
+assert.notStrictEqual(
+ child_process.spawnSync(appExecutablePath, [
+ '-e', 'fs.readdirSync(process.argv[1])', os.homedir()
+ ]).status,
+ 0);
+
+if (process.stdin.isTTY) {
+ // Run the sandboxed node instance with inherited tty stdin
+ const spawnResult = child_process.spawnSync(
+ appExecutablePath, ['-e', ''],
+ { stdio: 'inherit' }
+ );
+
+ assert.strictEqual(spawnResult.signal, null);
+}