Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs-site/src/content/docs/guides/local-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ Put plugin files in `plugins/` inside the opencodex home (`~/.opencodex/plugins/
`chmod go-w ~/.opencodex/plugins ~/.opencodex/plugins/*`; on systems whose default umask is
`002`, check the parent directories too. On macOS, an ACL grant to another user or group that
can write, delete, change permissions, or add/remove path entries blocks loading, even if the
mode is `0600`; inspect with `ls -le`. Read-only, deny, inheritance-only, and grants only to
the path owner, the running user, or root do not block loading. On Linux, extended ACLs are
checked when `getfacl` is installed. Without it, only owner and mode bits are verified.
mode is `0600`; inspect the path itself with `/bin/ls -lebd -- <path>`. Read-only, deny,
inheritance-only, and grants only to the path owner or running user do not block loading. ACL
display names such as `root` or `0` are not treated as numeric UID proof. On Linux, extended
ACLs are checked when `getfacl` is installed. Without it, only owner and mode bits are verified.
- On Windows automatic plugin loading is disabled until an ACL trust check is available.

Restart the proxy after adding, changing or removing a plugin (`ocx service restart`, or stop and
Expand Down
8 changes: 5 additions & 3 deletions src/plugins/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,12 @@ export function macAclListingTrustError(listing: string, currentUser = userInfo(
if (!entry) { unparseable = true; continue; }
if (entry[2] === "deny") continue;
const principal = entry[1]!;
// The file owner, this process's user, and root already control the path without an ACE.
// Require the `user:` prefix: a bare or group principal might include other users.
// The file owner and this process's user already control the path without an ACE. Require the
// `user:` prefix: a bare or group principal might include other users. macOS `ls` prints a
// resolved directory-record NAME here, so neither `0` nor `root` proves that the ACE is UID 0.
// On a genuinely root-owned path, `root` is still admitted by the owner comparison.
if (principal.startsWith("user:")
&& [owner, currentUser, "root", "0"].includes(principal.slice(5))) continue;
&& [owner, currentUser].includes(principal.slice(5))) continue;
const rights = entry[3]!.split(",");
if (rights.includes("only_inherit")) continue;
if (rights.some(right => !MAC_ACL_BENIGN_TOKENS.has(right))) return "has an access control list";
Expand Down
8 changes: 5 additions & 3 deletions structure/ops/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ or signs them.
can swap a checked path before it is imported; files are imported through the resolved directory.
On macOS, `ls -lebd` must show no effective non-owner ACL grant that can write, delete, change
permissions, or add/remove path entries on the file, plugin directory, or any ancestor. Denials,
grants only to the path owner, the running user, or root, read-only grants, and inheritance-only
entries on the inspected path are safe; inherited grants effective on a descendant are checked
at that descendant. A timed-out macOS inspection retries once only if its output is empty:
grants only to the path owner or running user, read-only grants, and inheritance-only entries on
the inspected path are safe; inherited grants effective on a descendant are checked at that
descendant. `ls` renders UUID-backed principals as Directory Services record names, so names
such as `root` or `0` never establish UID 0; root-owned paths still pass through the owner check.
A timed-out macOS inspection retries once only if its output is empty:
observed unsafe grants refuse immediately, and any other partial output is incomplete and also
refuses loading. Unknown grants or other inspection errors also refuse loading.
Linux uses `getfacl` when installed and refuses extended
Expand Down
24 changes: 23 additions & 1 deletion tests/lib/plugin-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,31 @@ test("recorded macOS ls output rejects effective non-owner write grants", () =>
+ " 0: group:everyone allow write\n";
const ownerNamedGroup = "drwx------@ 2 runner staff 64 Sep 27 07:50 /private/var/folders/ab/tmp/plugins\n"
+ " 0: group:runner allow add_file\n";
for (const listing of [pluginDir, ownedAncestor, inheritedChild, pluginFile, ownerNamedGroup]) {
// `/bin/ls -lebd` renders resolved ACL record names, not numeric UIDs. A foreign record named
// `0` must not inherit root trust merely because its name looks like UID 0 (#6017).
const numericRecordName = "-rw-------@ 1 runner staff 64 Sep 27 07:50 /plugins/plugin.ts\n"
+ " 0: user:0 allow write\n";
const rootRecordName = "-rw-------@ 1 runner staff 64 Sep 27 07:50 /plugins/plugin.ts\n"
+ " 0: user:root allow write\n";
const unresolvedUuid = "-rw-------@ 1 runner staff 64 Sep 27 07:50 /plugins/plugin.ts\n"
+ " 0: user:8D95C9F2-3B29-4B30-8932-C43D3AABC123 allow write\n";
for (const listing of [
pluginDir, ownedAncestor, inheritedChild, pluginFile, ownerNamedGroup,
numericRecordName, rootRecordName, unresolvedUuid,
]) {
expect(macAclListingTrustError(listing)).toBe("has an access control list");
}
// Bare principals are not identity-bearing user records. Keep the rights policy explicit so a
// future parser cleanup cannot accidentally grant them the owner/current-user exemption.
const bareBenignPrincipal = "-rw-------@ 1 runner staff 64 Sep 27 07:50 /plugins/plugin.ts\n"
+ " 0: runner allow read\n";
expect(macAclListingTrustError(bareBenignPrincipal)).toBeNull();
const bareWritePrincipal = "-rw-------@ 1 runner staff 64 Sep 27 07:50 /plugins/plugin.ts\n"
+ " 0: runner allow write\n";
expect(macAclListingTrustError(bareWritePrincipal)).toBe("has an access control list");
const numericCurrentUser = "-rw-------@ 1 0 staff 64 Sep 27 07:50 /plugins/plugin.ts\n"
+ " 0: user:0 allow write\n";
expect(macAclListingTrustError(numericCurrentUser, "0")).toBeNull();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
expect(macAclListingTrustError("drwxr-xr-x+ 23 root wheel 736 Sep 27 07:50 /\n 0: unrecognized ACL entry\n"))
.toBe("access control list inspection failed");
expect(macAclListingTrustError(`${pluginDir.split("\n")[0]}\n 0: group:everyone allow future_permission\n`))
Expand Down
Loading