I suspect this was done originally so that `execForSandbox()` had a consistent signature for both the `SandboxType.NONE` and `SandboxType.MACOS_SEATBELT` cases, but that is not really necessary and turns out to make the upcoming Landlock support a bit more complicated to implement, so I had Codex remove it and clean up the call sites.
149 lines
4.5 KiB
TypeScript
149 lines
4.5 KiB
TypeScript
import type { ExecResult } from "./interface.js";
|
|
import type { SpawnOptions } from "child_process";
|
|
|
|
import { exec } from "./raw-exec.js";
|
|
import { log } from "../../logger/log.js";
|
|
|
|
function getCommonRoots() {
|
|
return [
|
|
// Without this root, it'll cause:
|
|
// pyenv: cannot rehash: $HOME/.pyenv/shims isn't writable
|
|
`${process.env["HOME"]}/.pyenv`,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* When working with `sandbox-exec`, only consider `sandbox-exec` in `/usr/bin`
|
|
* to defend against an attacker trying to inject a malicious version on the
|
|
* PATH. If /usr/bin/sandbox-exec has been tampered with, then the attacker
|
|
* already has root access.
|
|
*/
|
|
export const PATH_TO_SEATBELT_EXECUTABLE = "/usr/bin/sandbox-exec";
|
|
|
|
export function execWithSeatbelt(
|
|
cmd: Array<string>,
|
|
opts: SpawnOptions,
|
|
writableRoots: ReadonlyArray<string>,
|
|
abortSignal?: AbortSignal,
|
|
): Promise<ExecResult> {
|
|
let scopedWritePolicy: string;
|
|
let policyTemplateParams: Array<string>;
|
|
|
|
const fullWritableRoots = [...writableRoots, ...getCommonRoots()];
|
|
// In practice, fullWritableRoots will be non-empty, but we check just in
|
|
// case the logic to build up fullWritableRoots changes.
|
|
if (fullWritableRoots.length > 0) {
|
|
const { policies, params } = fullWritableRoots
|
|
.map((root, index) => ({
|
|
policy: `(subpath (param "WRITABLE_ROOT_${index}"))`,
|
|
param: `-DWRITABLE_ROOT_${index}=${root}`,
|
|
}))
|
|
.reduce(
|
|
(
|
|
acc: { policies: Array<string>; params: Array<string> },
|
|
{ policy, param },
|
|
) => {
|
|
acc.policies.push(policy);
|
|
acc.params.push(param);
|
|
return acc;
|
|
},
|
|
{ policies: [], params: [] },
|
|
);
|
|
|
|
scopedWritePolicy = `\n(allow file-write*\n${policies.join(" ")}\n)`;
|
|
policyTemplateParams = params;
|
|
} else {
|
|
scopedWritePolicy = "";
|
|
policyTemplateParams = [];
|
|
}
|
|
|
|
const fullPolicy = READ_ONLY_SEATBELT_POLICY + scopedWritePolicy;
|
|
log(
|
|
`Running seatbelt with policy: ${fullPolicy} and ${
|
|
policyTemplateParams.length
|
|
} template params: ${policyTemplateParams.join(", ")}`,
|
|
);
|
|
|
|
const fullCommand = [
|
|
PATH_TO_SEATBELT_EXECUTABLE,
|
|
"-p",
|
|
fullPolicy,
|
|
...policyTemplateParams,
|
|
"--",
|
|
...cmd,
|
|
];
|
|
return exec(fullCommand, opts, abortSignal);
|
|
}
|
|
|
|
const READ_ONLY_SEATBELT_POLICY = `
|
|
(version 1)
|
|
|
|
; inspired by Chrome's sandbox policy:
|
|
; https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/common.sb;l=273-319;drc=7b3962fe2e5fc9e2ee58000dc8fbf3429d84d3bd
|
|
|
|
; start with closed-by-default
|
|
(deny default)
|
|
|
|
; allow read-only file operations
|
|
(allow file-read*)
|
|
|
|
; child processes inherit the policy of their parent
|
|
(allow process-exec)
|
|
(allow process-fork)
|
|
(allow signal (target self))
|
|
|
|
(allow file-write-data
|
|
(require-all
|
|
(path "/dev/null")
|
|
(vnode-type CHARACTER-DEVICE)))
|
|
|
|
; sysctls permitted.
|
|
(allow sysctl-read
|
|
(sysctl-name "hw.activecpu")
|
|
(sysctl-name "hw.busfrequency_compat")
|
|
(sysctl-name "hw.byteorder")
|
|
(sysctl-name "hw.cacheconfig")
|
|
(sysctl-name "hw.cachelinesize_compat")
|
|
(sysctl-name "hw.cpufamily")
|
|
(sysctl-name "hw.cpufrequency_compat")
|
|
(sysctl-name "hw.cputype")
|
|
(sysctl-name "hw.l1dcachesize_compat")
|
|
(sysctl-name "hw.l1icachesize_compat")
|
|
(sysctl-name "hw.l2cachesize_compat")
|
|
(sysctl-name "hw.l3cachesize_compat")
|
|
(sysctl-name "hw.logicalcpu_max")
|
|
(sysctl-name "hw.machine")
|
|
(sysctl-name "hw.ncpu")
|
|
(sysctl-name "hw.nperflevels")
|
|
(sysctl-name "hw.optional.arm.FEAT_BF16")
|
|
(sysctl-name "hw.optional.arm.FEAT_DotProd")
|
|
(sysctl-name "hw.optional.arm.FEAT_FCMA")
|
|
(sysctl-name "hw.optional.arm.FEAT_FHM")
|
|
(sysctl-name "hw.optional.arm.FEAT_FP16")
|
|
(sysctl-name "hw.optional.arm.FEAT_I8MM")
|
|
(sysctl-name "hw.optional.arm.FEAT_JSCVT")
|
|
(sysctl-name "hw.optional.arm.FEAT_LSE")
|
|
(sysctl-name "hw.optional.arm.FEAT_RDM")
|
|
(sysctl-name "hw.optional.arm.FEAT_SHA512")
|
|
(sysctl-name "hw.optional.armv8_2_sha512")
|
|
(sysctl-name "hw.memsize")
|
|
(sysctl-name "hw.pagesize")
|
|
(sysctl-name "hw.packages")
|
|
(sysctl-name "hw.pagesize_compat")
|
|
(sysctl-name "hw.physicalcpu_max")
|
|
(sysctl-name "hw.tbfrequency_compat")
|
|
(sysctl-name "hw.vectorunit")
|
|
(sysctl-name "kern.hostname")
|
|
(sysctl-name "kern.maxfilesperproc")
|
|
(sysctl-name "kern.osproductversion")
|
|
(sysctl-name "kern.osrelease")
|
|
(sysctl-name "kern.ostype")
|
|
(sysctl-name "kern.osvariant_status")
|
|
(sysctl-name "kern.osversion")
|
|
(sysctl-name "kern.secure_kernel")
|
|
(sysctl-name "kern.usrstack64")
|
|
(sysctl-name "kern.version")
|
|
(sysctl-name "sysctl.proc_cputype")
|
|
(sysctl-name-prefix "hw.perflevel")
|
|
)`.trim();
|