fix(sandbox): bind parent dir for globstar allow_write on bwrap

Fine-grained per-path mounts under read-only project binds broke pip
install into in-project .venv directories. Always mount the parent tree
for ** write rules instead.

Fixes #315
This commit is contained in:
Sahilb315
2026-06-06 18:09:50 +05:30
parent f3e00a7f6e
commit e2e740dd76
2 changed files with 51 additions and 9 deletions
@@ -529,8 +529,19 @@ func (t *bubblewrapPolicyTranslator) processWriteRule(path string, boundPaths ma
// Check if path contains glob pattern
if util.ContainsGlob(path) {
// Check if the base directory is already bound (e.g., /tmp already bound, skip /tmp/**)
baseDir := t.extractParentDir(path)
// Globstar write rules always bind the parent directory read-write. Per-path
// binds interact badly with earlier read-only parent mounts (e.g. ${CWD}/**)
// and miss files beyond maxGlobDepth — see https://github.com/safedep/pmg/issues/315.
if strings.Contains(path, "**") {
args = append(args, "--bind-try", baseDir, baseDir)
boundPaths[baseDir] = true
log.Debugf("Globstar allow_write: bound parent directory '%s' (read-write)", baseDir)
return args, nil
}
// Check if the base directory is already bound (e.g., /tmp already bound, skip /tmp/**)
if boundPaths[baseDir] {
log.Debugf("Skipping pattern '%s' - base directory '%s' already bound", path, baseDir)
return args, nil
@@ -545,14 +556,9 @@ func (t *bubblewrapPolicyTranslator) processWriteRule(path string, boundPaths ma
if useFallback {
// Coarse-grained: bind parent directory
for _, parentDir := range paths {
if !boundPaths[parentDir] {
args = append(args, "--bind-try", parentDir, parentDir)
boundPaths[parentDir] = true
log.Debugf("Coarse-grained fallback: bound parent directory '%s' (read-write)", parentDir)
} else {
// Path already bound, skip (likely already bound as read-only from essential paths)
log.Debugf("Parent directory '%s' already bound, skipping duplicate bind", parentDir)
}
args = append(args, "--bind-try", parentDir, parentDir)
boundPaths[parentDir] = true
log.Debugf("Coarse-grained fallback: bound parent directory '%s' (read-write)", parentDir)
}
} else {
// Fine-grained: bind individual paths
@@ -775,6 +775,42 @@ func TestGlobFallbackThresholdGlobstar(t *testing.T) {
assert.Less(t, len(args), 300, "Coarse-grained fallback should prevent argument explosion")
}
// TestBubblewrapAllowWriteGlobstarBindsParentOnly verifies globstar allow_write rules
// bind the parent tree (e.g. .venv) instead of per-file mounts that break pip in-project venvs.
// See https://github.com/safedep/pmg/issues/315
func TestBubblewrapAllowWriteGlobstarBindsParentOnly(t *testing.T) {
tmpDir := t.TempDir()
venvDir := filepath.Join(tmpDir, ".venv")
binDir := filepath.Join(venvDir, "bin")
require.NoError(t, os.MkdirAll(binDir, 0755))
pythonPath := filepath.Join(binDir, "python")
require.NoError(t, os.Symlink("/usr/bin/python3", pythonPath))
// Populate enough shallow files that old logic would fine-grain bind without hitting fallback.
for i := 0; i < 30; i++ {
path := filepath.Join(venvDir, fmt.Sprintf("file%d.txt", i))
require.NoError(t, os.WriteFile(path, []byte("x"), 0644))
}
config := newDefaultBubblewrapConfig()
translator := newBubblewrapPolicyTranslator(config)
policy := &sandbox.SandboxPolicy{
Name: "test-venv-write",
Filesystem: sandbox.FilesystemPolicy{
AllowRead: []string{tmpDir + "/**"},
AllowWrite: []string{venvDir + "/**"},
},
}
args, err := translator.translate(policy)
require.NoError(t, err)
argsStr := argSliceToString(args)
assert.Contains(t, argsStr, venvDir)
assert.NotContains(t, argsStr, pythonPath)
}
// TestGlobNoFallbackSmallPattern tests that small patterns don't trigger fallback
func TestGlobNoFallbackSmallPattern(t *testing.T) {
tmpDir := t.TempDir()