Files
pmg/sandbox/platform/glob_linux.go
T
Sahil BansalandGitHub 872c5d663c fix(sandbox): bind parent dir for globstar allow_write on bwrap (#321)
* 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

* test(sandbox): tighten globstar bind assertions and ensure ~/.npm exists for e2e

Strengthen TestBubblewrapAllowWriteGlobstarBindsParentOnly to verify the
parent dir is writably bound and the child path is read-only bound, not
just substring presence. Pre-create ~/.npm in the e2e harness so
bubblewrap --bind-try does not skip the npm cache dir on fresh runners.

* switch pnpm to /tmp in sandbox e2e

* test(sandbox): update glob ** test for parent-bind semantics

Globstar allow_write now binds the parent dir only (e2e740d), so the
test should assert the parent is writably bound and child subdirs are
not individually bound, instead of substring-matching subdir names.

* fix(sandbox): bind correct base dir for in-pattern globstar allow_write

Globstar allow_write previously used extractGlobParentDir, which walks past
the first ** and yields the wrong root for patterns like /a/b/**/d/**/e.
Introduce extractGlobstarWriteBaseDir, which takes the prefix before the
first /**, and use it in processWriteRule. Also dedup the coarse-fallback
parent-bind loop to mirror the read-rule fallback.
2026-06-07 10:03:23 +05:30

127 lines
3.9 KiB
Go

//go:build linux
package platform
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/safedep/dry/log"
"github.com/safedep/pmg/sandbox/util"
)
// expandGlobstarPattern expands patterns containing ** (recursive glob),
// which filepath.Glob does not support. Splits the pattern at ** into a base
// path and a suffix, walks the base with a depth limit, and collects entries
// whose path ends with the suffix.
//
// If the base path does not yet exist, returns []string{basePath} so callers
// can still grant coverage to the parent directory (matters for fresh
// node_modules / pnpm caches that haven't been created yet).
func expandGlobstarPattern(pattern string, maxDepth, maxPaths int) ([]string, error) {
parts := strings.Split(pattern, "**")
if len(parts) != 2 {
return nil, fmt.Errorf("only one ** globstar supported per pattern")
}
basePath := strings.TrimSuffix(parts[0], "/")
suffix := strings.TrimPrefix(parts[1], "/")
if basePath == "" {
log.Debugf("Skipping globstar pattern '%s' with empty base path (would walk from root)", pattern)
return []string{}, nil
}
expandedBase, err := util.ExpandVariables(basePath)
if err != nil {
return nil, fmt.Errorf("failed to expand base path: %w", err)
}
basePath = expandedBase
if _, err := os.Stat(basePath); os.IsNotExist(err) {
return []string{basePath}, nil
}
matches := []string{}
if err := walkGlobWithDepthLimit(basePath, suffix, maxDepth, maxPaths, &matches); err != nil {
return nil, fmt.Errorf("failed to walk directory tree: %w", err)
}
return matches, nil
}
// walkGlobWithDepthLimit walks a directory tree from root, appending paths
// whose suffix matches `suffix`. Stops at maxDepth levels (when > 0) and
// after collecting maxPaths entries.
func walkGlobWithDepthLimit(root, suffix string, maxDepth, maxPaths int, matches *[]string) error {
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
relPath, err := filepath.Rel(root, path)
if err != nil {
return nil
}
depth := 0
if relPath != "." {
depth = len(strings.Split(relPath, string(filepath.Separator)))
}
if maxDepth > 0 && depth > maxDepth {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
if suffix == "" || strings.HasSuffix(path, suffix) {
*matches = append(*matches, path)
if len(*matches) >= maxPaths {
return filepath.SkipAll
}
}
return nil
})
}
// extractGlobParentDir extracts the parent directory of a glob pattern. Used
// for coarse-grained fallback when expansion yields too many paths or the
// target tree is unsuitable for fine-grained rules.
//
// Examples:
// - ${CWD}/node_modules/** → ${CWD}/node_modules
// - ${HOME}/.cache/pnpm/** → ${HOME}/.cache/pnpm
// - /tmp/*.txt → /tmp
// - /usr/lib/**/*.so → /usr/lib
func extractGlobParentDir(pattern string) string {
pattern = strings.TrimSuffix(pattern, "/**")
pattern = strings.TrimSuffix(pattern, "/*")
idx := strings.IndexAny(pattern, "*?[")
if idx >= 0 {
pattern = pattern[:idx]
pattern = filepath.Dir(pattern)
}
pattern = strings.TrimSuffix(pattern, string(filepath.Separator))
if pattern == "" || pattern == string(filepath.Separator) {
return "."
}
return pattern
}
// extractGlobstarWriteBaseDir returns the directory to bind read-write for an
// allow_write globstar pattern. It uses the path before the first "/**" so
// suffix-only profiles (${CWD}/.venv/**) and in-pattern globstars (/a/b/**/d/**/e)
// both bind the intended tree root (/a/b for the latter). Falls back to
// extractGlobParentDir when the pattern has ** but no "/**" segment.
func extractGlobstarWriteBaseDir(pattern string) string {
if i := strings.Index(pattern, "/**"); i >= 0 {
base := strings.TrimSuffix(pattern[:i], string(filepath.Separator))
if base == "" {
return string(filepath.Separator)
}
return base
}
return extractGlobParentDir(pattern)
}