mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
Fix glob parent directory allowance for patterns with glob characters (#331)
* fix(sandbox): support pnpm workspaces and macOS cache dir in pnpm profile pnpm in a workspace (monorepo) creates a node_modules directory inside every workspace package to symlink direct dependencies. The profile only allowed writes to the root node_modules, so installs failed with EPERM on mkdir of e.g. apps/mobile/node_modules. pnpm on macOS also writes its cache (lockfile verification, metadata) under ~/Library/Caches/pnpm, while the base profile only covers the XDG path ~/.cache/pnpm. Fixes are scoped to the pnpm leaf profile, not the shared npm-restrictive base. Ref: https://github.com/safedep/pmg/issues/329 * fix(sandbox): emit regex parent rule for nested-glob allow patterns on Seatbelt For allow patterns ending in /**, the translator auto-allows the parent directory so mkdir/stat of the directory itself succeeds. The rule was always emitted as a literal, which can never match when the parent still contains glob characters (e.g. ${CWD}/**/node_modules from a workspace allowance) — silently leaving the directory's own creation denied. Emit a regex rule for glob-bearing parents instead. This stays strictly narrower than the Linux drivers (Bubblewrap binds the prefix before the first /** read-write; Landlock grants the glob expansion or its parent), and deny rules are emitted after allows, so mandatory credential denies still override. Ref: https://github.com/safedep/pmg/issues/329 --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -109,8 +109,16 @@ func getAncestorDirectories(pathStr string) []string {
|
||||
return ancestors
|
||||
}
|
||||
|
||||
// globDoubleStarAutoAllowParentDirIfNeeded checks if a pattern ends with /** and emits a literal rule for the parent directory.
|
||||
// globDoubleStarAutoAllowParentDirIfNeeded checks if a pattern ends with /** and emits a rule for the parent directory.
|
||||
// This ensures operations like mkdir('dir') or stat('dir') succeed before accessing dir/** contents.
|
||||
//
|
||||
// When the parent itself still contains glob characters (e.g. ${CWD}/**/node_modules),
|
||||
// a literal rule can never match a real path, so a regex rule is emitted instead.
|
||||
// This grants access only to directories matching the parent pattern — strictly
|
||||
// narrower than the Linux drivers, which bind the prefix before the first /**
|
||||
// (Bubblewrap) or the glob's expansion/parent (Landlock) read-write. Deny rules
|
||||
// are emitted after allows and override them, so mandatory credential denies
|
||||
// are unaffected.
|
||||
func globDoubleStarAutoAllowParentDirIfNeeded(sb *strings.Builder, pattern string, expanded string, operation string) {
|
||||
if !strings.HasSuffix(expanded, "/**") {
|
||||
return
|
||||
@@ -127,6 +135,12 @@ func globDoubleStarAutoAllowParentDirIfNeeded(sb *strings.Builder, pattern strin
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString("(allow ")
|
||||
sb.WriteString(operation)
|
||||
if util.ContainsGlob(parentDir) {
|
||||
sb.WriteString(" (regex #\"")
|
||||
sb.WriteString(util.GlobToRegex(parentDir))
|
||||
sb.WriteString("\"))\n")
|
||||
return
|
||||
}
|
||||
sb.WriteString(" (literal \"")
|
||||
sb.WriteString(parentDir)
|
||||
sb.WriteString("\"))\n")
|
||||
|
||||
@@ -107,6 +107,22 @@ func TestSeatbeltTranslatorDarwinFilesystemTranslation(t *testing.T) {
|
||||
assert.Contains(t, actual, "^/path/to/dir/.*$")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "glob pattern with /** and glob parent",
|
||||
policy: &sandbox.SandboxPolicy{
|
||||
Filesystem: sandbox.FilesystemPolicy{
|
||||
AllowWrite: []string{"/project/**/node_modules/**"},
|
||||
},
|
||||
},
|
||||
assert: func(t *testing.T, actual string, err error) {
|
||||
assert.NoError(t, err)
|
||||
// Parent still contains a glob, so the auto-allow must be a
|
||||
// regex rule — a literal with ** can never match a real path.
|
||||
assert.Contains(t, actual, `(allow file-write* (regex #"^/project/(.*/)?node_modules$"))`)
|
||||
assert.Contains(t, actual, `^/project/(.*/)?node_modules/.*$`)
|
||||
assert.NotContains(t, actual, `(literal "/project/**/node_modules")`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "glob pattern with *.txt",
|
||||
policy: &sandbox.SandboxPolicy{
|
||||
|
||||
@@ -19,10 +19,16 @@ environment:
|
||||
- NODE_EXTRA_CA_CERTS
|
||||
|
||||
filesystem:
|
||||
allow_read:
|
||||
# pnpm cache on macOS lives under ~/Library/Caches (lockfile verification,
|
||||
# metadata). The base profile only covers the XDG path ~/.cache/pnpm.
|
||||
- ${HOME}/Library/Caches/pnpm/**
|
||||
|
||||
allow_write:
|
||||
# pnpm needs write access here
|
||||
- ${HOME}/Library/pnpm/.tools/**
|
||||
- ${HOME}/.pnpm-store/**
|
||||
- ${HOME}/Library/Caches/pnpm/**
|
||||
|
||||
# `pnpm i` creates the tmp files in local dir, at least on MacOS
|
||||
- ${CWD}/_tmp_*
|
||||
@@ -39,4 +45,13 @@ filesystem:
|
||||
# Need access for dependency resolution
|
||||
- ${CWD}/.pnpm-store
|
||||
|
||||
# Workspaces (monorepos): pnpm creates a node_modules inside every
|
||||
# workspace package to symlink its direct dependencies. The bare
|
||||
# **/node_modules form is required in addition to **/node_modules/**:
|
||||
# the automatic parent-directory allowance only works for literal
|
||||
# parents, and this parent contains a glob, so mkdir of the directory
|
||||
# itself must be matched explicitly.
|
||||
- ${CWD}/**/node_modules
|
||||
- ${CWD}/**/node_modules/**
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user