From 2a47bdb8e74e34ae633e5502a88461063f069851 Mon Sep 17 00:00:00 2001 From: Arthur Bogaart <526197+abogaart@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:28:23 +0200 Subject: [PATCH] fix: spawn Apple /bin/chmod by absolute path when hardening credentials The macOS credential hardener strips ACLs with `chmod -N`, spawning `chmod` by bare name. On machines where Nix home-manager or Homebrew coreutils sit ahead of /bin on PATH, that resolves to GNU chmod, which has no `-N` flag and exits non-zero. The hardener treated that as a failure and refused every save from the Provider Settings panel with "could not restrict the credential file to your account; nothing was saved". Spawn /bin/chmod by absolute path instead, matching how the Windows branch already resolves its native tools without consulting PATH, and add a regression test that pins the absolute path. --- server/standalone/key-setup-hardening.mjs | 6 +++++- src/keySetupHardening.test.mjs | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/server/standalone/key-setup-hardening.mjs b/server/standalone/key-setup-hardening.mjs index 002ccabdc..e2d11182c 100644 --- a/server/standalone/key-setup-hardening.mjs +++ b/server/standalone/key-setup-hardening.mjs @@ -123,7 +123,11 @@ export function hardenCredentialFile( if (platform !== 'win32') { try { if (platform === 'darwin') { - const aclRemoval = spawn('chmod', ['-N', filepath], { + // `-N` (strip the ACL) is Apple-only. Spawn /bin/chmod by absolute + // path, never by name: a Nix/Homebrew coreutils profile puts GNU chmod + // first on PATH, and GNU chmod rejects `-N` — which read as a + // hardening failure and refused every save on such machines. + const aclRemoval = spawn('/bin/chmod', ['-N', filepath], { stdio: 'ignore', }); if (!commandCompletedSuccessfully(aclRemoval)) return false; diff --git a/src/keySetupHardening.test.mjs b/src/keySetupHardening.test.mjs index 2578be37d..c519576e3 100644 --- a/src/keySetupHardening.test.mjs +++ b/src/keySetupHardening.test.mjs @@ -55,6 +55,21 @@ test('macOS ACL removal failure stops before chmod and fails closed', () => { assert.deepEqual(fileSystem.calls, [], 'mode bits must not disguise an ACL-removal failure'); }); +test('macOS ACL removal spawns Apple /bin/chmod by absolute path, not via PATH', () => { + const spawned = []; + hardenCredentialFile(FILE, { + platform: 'darwin', + fileSystem: fileSystemWithMode(0o100600), + spawn(command, args) { + spawned.push([command, args]); + return { status: 0, signal: null }; + }, + }); + // A coreutils-first PATH (Nix home-manager, Homebrew) resolves bare `chmod` + // to GNU chmod, which has no `-N` and would fail every save. + assert.deepEqual(spawned, [['/bin/chmod', ['-N', FILE]]]); +}); + test('POSIX hardening verifies the resulting 0600 mode', () => { const goodFs = fileSystemWithMode(0o100600); assert.equal(hardenCredentialFile(FILE, {