2026-01-15 20:12:12 +05:30
//go:build linux
// +build linux
package platform
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/safedep/dry/log"
"github.com/safedep/dry/utils"
"github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/sandbox/util"
)
// bubblewrapPolicyTranslator translates PMG SandboxPolicy to Bubblewrap (bwrap) CLI arguments.
//
// Bubblewrap uses command-line arguments instead of profile files (like Seatbelt).
// The translator generates arguments for:
// - Filesystem bind mounts (--bind, --ro-bind, --dev-bind)
// - Network isolation (--unshare-net)
// - Process isolation (--unshare-pid, --unshare-ipc)
// - Device access (--dev-bind /dev/null, etc.)
// - Essential system permissions
type bubblewrapPolicyTranslator struct {
config * bubblewrapConfig
}
// newBubblewrapPolicyTranslator creates a new translator with the given config.
func newBubblewrapPolicyTranslator ( config * bubblewrapConfig ) * bubblewrapPolicyTranslator {
return & bubblewrapPolicyTranslator {
config : config ,
}
}
// translate converts a PMG SandboxPolicy to bwrap CLI arguments.
// Returns a slice of arguments to pass to the bwrap command.
func ( t * bubblewrapPolicyTranslator ) translate ( policy * sandbox . SandboxPolicy ) ([] string , error ) {
args := [] string {}
// 1. Add essential system permissions (filesystem, devices, proc)
systemArgs , err := t . addEssentialSystemPermissions ()
if err != nil {
return nil , fmt . Errorf ( "failed to add essential system permissions: %w" , err )
}
args = append ( args , systemArgs ... )
// 2. Add isolation namespaces
isolationArgs := t . addIsolationNamespaces ( policy )
args = append ( args , isolationArgs ... )
// 3. Add filesystem rules (allow read, allow write, deny patterns)
filesystemArgs , err := t . translateFilesystem ( policy )
if err != nil {
return nil , fmt . Errorf ( "failed to translate filesystem rules: %w" , err )
}
args = append ( args , filesystemArgs ... )
// 4. Add PTY support if needed
if utils . SafelyGetValue ( policy . AllowPTY ) {
ptyArgs := t . addPTYSupport ()
args = append ( args , ptyArgs ... )
}
// 5. Add tmpdir support (package managers need writable temp directory)
tmpdirArgs := t . addTmpdirSupport ()
args = append ( args , tmpdirArgs ... )
// 6. Check total argument limit and log warning if exceeded
// Do not fail, let bwrap fail naturally if it does.
if len ( args ) > t . config . totalArgsLimit {
log . Warnf ( "Total bwrap arguments (%d) exceeds safety limit (%d), sandbox may fail with 'Argument list too long' error" ,
len ( args ), t . config . totalArgsLimit )
}
log . Debugf ( "Translated policy '%s' to %d bwrap arguments (limit: %d)" , policy . Name , len ( args ), t . config . totalArgsLimit )
return args , nil
}
// addEssentialSystemPermissions adds bind mounts for essential system paths and devices
// that package managers need to function properly.
func ( t * bubblewrapPolicyTranslator ) addEssentialSystemPermissions () ([] string , error ) {
args := [] string {}
// Add essential system paths (read-only)
for _ , path := range t . config . getEssentialSystemPaths () {
args = append ( args , "--ro-bind-try" , path , path )
}
// Add essential device files
for _ , device := range t . config . getEssentialDevices () {
args = append ( args , "--dev-bind-try" , device , device )
}
// Add proc filesystem (read-only for safety)
for _ , procPath := range t . config . procPaths {
args = append ( args , "--proc" , procPath )
}
return args , nil
}
// addIsolationNamespaces adds namespace isolation arguments based on policy and config.
func ( t * bubblewrapPolicyTranslator ) addIsolationNamespaces ( policy * sandbox . SandboxPolicy ) [] string {
args := [] string {}
// Network isolation
hasAllowRules := len ( policy . Network . AllowOutbound ) > 0
hasDenyAll := false
for _ , pattern := range policy . Network . DenyOutbound {
if pattern == "*:*" {
hasDenyAll = true
break
}
}
if t . config . shouldUnshareNetwork ( hasAllowRules , hasDenyAll ) {
args = append ( args , "--unshare-net" )
log . Debugf ( "Network isolated (--unshare-net)" )
} else {
log . Debugf ( "Network allowed (no --unshare-net)" )
}
2026-02-04 15:32:47 +05:30
// Note: AllowNetworkBind and Network.AllowBind are not handled here because
// bwrap's --unshare-net creates a namespace with loopback available, so
// localhost binding already works. Non-localhost binding requires full host
// network (no --unshare-net), which is controlled by AllowOutbound rules.
2026-01-15 20:12:12 +05:30
// PID namespace isolation
if t . config . unsharePID {
args = append ( args , "--unshare-pid" )
}
// IPC namespace isolation
if t . config . unshareIPC {
args = append ( args , "--unshare-ipc" )
}
// New session
if t . config . newSession {
args = append ( args , "--new-session" )
}
// Die with parent
if t . config . dieWithParent {
args = append ( args , "--die-with-parent" )
}
return args
}
// translateFilesystem converts filesystem policy rules to bwrap bind mount arguments.
//
// Bubblewrap filesystem isolation works via bind mounts:
// - --ro-bind: Read-only bind mount
// - --bind: Read-write bind mount
// - --dev-bind: Device file bind mount
// - --tmpfs: Temporary file system mount (used to hide specific files/directories)
// - Paths not mounted are inaccessible (deny-by-default)
//
// Strategy:
// 1. Start with essential system paths (added separately)
// 2. Add user-specified allow_read paths FIRST (read-only bind mounts)
// This establishes the base filesystem view (e.g., "/" for full access)
// 3. Add user-specified allow_write paths SECOND (read-write bind mounts)
// These OVERRIDE earlier read-only binds (bwrap: later mounts win)
// 4. Handle deny patterns by mounting /dev/null or read-only for directories
// 5. Add mandatory deny patterns
func ( t * bubblewrapPolicyTranslator ) translateFilesystem ( policy * sandbox . SandboxPolicy ) ([] string , error ) {
args := [] string {}
// Track paths we've already bound for read and write to avoid duplicates
// Bubblewrap later mounts win, so we need to track both read and write bound paths.
readBoundPaths := make ( map [ string ] bool )
writeBoundPaths := make ( map [ string ] bool )
// Add essential system paths to bound paths (already handled separately)
for _ , path := range t . config . getEssentialSystemPaths () {
readBoundPaths [ path ] = true
}
// Mark tmpdir as already bound (will be handled by addTmpdirSupport())
// This prevents conflicts from policy patterns like /tmp/**
tmpDir := os . TempDir ()
writeBoundPaths [ tmpDir ] = true
// 1. Process allow_read rules FIRST (read-only bind mounts)
// This establishes the base read-only filesystem view (including "/" if specified)
for _ , pattern := range policy . Filesystem . AllowRead {
expanded , err := util . ExpandVariables ( pattern )
if err != nil {
log . Warnf ( "Failed to expand variables in allow_read pattern '%s': %v" , pattern , err )
continue
}
// Glob chars are handled by the processReadRule function.
readArgs , err := t . processReadRule ( expanded , readBoundPaths )
if err != nil {
log . Warnf ( "Failed to process allow_read rule '%s': %v" , expanded , err )
continue
}
args = append ( args , readArgs ... )
}
// 2. Process allow_write rules SECOND (read-write bind mounts)
// These OVERRIDE earlier read-only binds (bwrap: later mounts win)
// Use a separate map so we don't skip paths that need write access
writeBoundPaths [ tmpDir ] = true // tmpdir handled by addTmpdirSupport
for _ , pattern := range policy . Filesystem . AllowWrite {
expanded , err := util . ExpandVariables ( pattern )
if err != nil {
log . Warnf ( "Failed to expand variables in allow_write pattern '%s': %v" , pattern , err )
continue
}
// Glob chars are handled by the processWriteRule function.
writeArgs , err := t . processWriteRule ( expanded , writeBoundPaths )
if err != nil {
log . Warnf ( "Failed to process allow_write rule '%s': %v" , expanded , err )
continue
}
args = append ( args , writeArgs ... )
}
2026-05-11 16:12:06 +05:30
// 3. Process deny_write rules after allow_write so read-only binds override writable parents.
2026-05-06 12:45:36 +05:30
expandedAllowRead , err := expandAll ( policy . Filesystem . AllowRead )
if err != nil {
log . Warnf ( "sandbox: failed to expand allow_read for mandatory deny suppression, all mandatory denies preserved: %v" , err )
expandedAllowRead = nil
}
expandedAllowWrite , err := expandAll ( policy . Filesystem . AllowWrite )
if err != nil {
log . Warnf ( "sandbox: failed to expand allow_write for mandatory deny suppression, all mandatory denies preserved: %v" , err )
expandedAllowWrite = nil
}
2026-01-15 20:12:12 +05:30
2026-05-06 12:45:36 +05:30
mandatoryResult := util . GetMandatoryDenyPatterns ( util . MandatoryDenyOptions {
AllowGitConfig : utils . SafelyGetValue ( policy . AllowGitConfig ),
AllowRead : expandedAllowRead ,
AllowWrite : expandedAllowWrite ,
})
2026-01-15 20:12:12 +05:30
2026-05-06 12:45:36 +05:30
for _ , p := range mandatoryResult . SuppressedRead {
log . Warnf ( "sandbox: mandatory deny %q suppressed for read by explicit allow rule in policy %q" , p , policy . Name )
}
for _ , p := range mandatoryResult . SuppressedWrite {
log . Warnf ( "sandbox: mandatory deny %q suppressed for write by explicit allow rule in policy %q" , p , policy . Name )
}
// bwrap has no primitive that denies reads while allowing writes — --bind
// exposes both, and read-blocking mounts (--tmpfs, --ro-bind /dev/null)
// also block writes. When the user opts out of write but not read for a
// mandatory path, the read-side deny is unenforceable; warn so it's not
// silent.
suppressedWriteSet := make ( map [ string ] bool , len ( mandatoryResult . SuppressedWrite ))
for _ , p := range mandatoryResult . SuppressedWrite {
suppressedWriteSet [ p ] = true
}
for _ , p := range mandatoryResult . DenyRead {
if suppressedWriteSet [ p ] {
log . Warnf ( "sandbox: read-side mandatory deny %q cannot be enforced on linux because allow_write for the same path exposes both read and write; consider also listing the path in allow_read if read access is intended, or remove from allow_write if not" , p )
}
}
2026-05-19 14:40:54 +05:30
for _ , pattern := range policy . Filesystem . DenyRead {
expanded , err := util . ExpandVariables ( pattern )
if err != nil {
log . Warnf ( "Failed to expand variables in deny_read pattern '%s': %v" , pattern , err )
continue
}
denyArgs , err := t . processDenyReadRule ( expanded )
if err != nil {
log . Debugf ( "Deny read rule '%s' skipped: %v" , expanded , err )
continue
}
args = append ( args , denyArgs ... )
}
2026-05-06 12:45:36 +05:30
for _ , pattern := range policy . Filesystem . DenyWrite {
2026-01-15 20:12:12 +05:30
expanded , err := util . ExpandVariables ( pattern )
if err != nil {
log . Warnf ( "Failed to expand variables in deny pattern '%s': %v" , pattern , err )
continue
}
2026-05-11 16:12:06 +05:30
denyArgs , err := t . processDenyWriteRule ( expanded )
2026-01-15 20:12:12 +05:30
if err != nil {
log . Debugf ( "Deny rule '%s' skipped: %v" , expanded , err )
continue
}
args = append ( args , denyArgs ... )
}
2026-05-06 12:45:36 +05:30
// Skip mandatory write denies for paths the user listed in allow_read: the
// allow_read --ro-bind already denies writes (EROFS), and overlaying
// /dev/null on top would also mask reads, breaking the read-side opt-out.
// User-listed deny_write entries above are unaffected — "deny wins" still
// applies to explicit user rules.
allowReadSet := make ( map [ string ] bool , len ( expandedAllowRead ))
for _ , p := range expandedAllowRead {
allowReadSet [ filepath . Clean ( p )] = true
}
for _ , pattern := range mandatoryResult . DenyWrite {
if allowReadSet [ filepath . Clean ( pattern )] {
continue
}
expanded , err := util . ExpandVariables ( pattern )
if err != nil {
log . Warnf ( "Failed to expand variables in deny pattern '%s': %v" , pattern , err )
continue
}
denyArgs , err := t . processDenyRule ( expanded )
if err != nil {
log . Debugf ( "Deny rule '%s' skipped: %v" , expanded , err )
continue
}
args = append ( args , denyArgs ... )
}
// 4. Tmpfs-hide credential directories. Tmpfs blocks both directions, so
// only paths denied on both sides qualify.
tmpfsCandidates := intersectStrings ( mandatoryResult . DenyRead , mandatoryResult . DenyWrite )
2026-01-15 20:12:12 +05:30
hiddenDirs := make ( map [ string ] bool )
2026-05-06 12:45:36 +05:30
for _ , pattern := range tmpfsCandidates {
2026-01-15 20:12:12 +05:30
expanded , err := util . ExpandVariables ( pattern )
if err != nil {
continue
}
var dirsToHide [] string
if util . ContainsGlob ( expanded ) {
matches , err := filepath . Glob ( expanded )
if err != nil {
continue
}
dirsToHide = matches
} else {
dirsToHide = [] string { expanded }
}
for _ , dir := range dirsToHide {
if hiddenDirs [ dir ] {
continue
}
if info , err := os . Stat ( dir ); err == nil && info . IsDir () {
args = append ( args , "--tmpfs" , dir )
hiddenDirs [ dir ] = true
log . Debugf ( "Hiding credential directory '%s' with tmpfs" , dir )
}
}
}
// 5. Process deny_exec rules (mount /dev/null over executables)
for _ , exePath := range policy . Process . DenyExec {
expanded , err := util . ExpandVariables ( exePath )
if err != nil {
log . Warnf ( "Failed to expand variables in deny_exec pattern '%s': %v" , exePath , err )
continue
}
// Handle glob patterns (e.g., /usr/bin/python*)
if util . ContainsGlob ( expanded ) {
matches , err := filepath . Glob ( expanded )
if err != nil {
log . Warnf ( "Failed to expand deny_exec glob '%s': %v" , expanded , err )
continue
}
for _ , match := range matches {
if info , err := os . Stat ( match ); err == nil && ! info . IsDir () {
args = append ( args , "--ro-bind" , "/dev/null" , match )
log . Debugf ( "Blocked execution of '%s'" , match )
}
}
} else {
if info , err := os . Stat ( expanded ); err == nil && ! info . IsDir () {
args = append ( args , "--ro-bind" , "/dev/null" , expanded )
log . Debugf ( "Blocked execution of '%s'" , expanded )
}
}
}
return args , nil
}
2026-05-19 14:40:54 +05:30
// processDenyReadRule hides readable content. Files are masked with /dev/null;
// directories are overlaid with tmpfs so their host contents are not visible.
func ( t * bubblewrapPolicyTranslator ) processDenyReadRule ( path string ) ([] string , error ) {
args := [] string {}
if util . ContainsGlob ( path ) {
if strings . Contains ( path , "**" ) {
parentDir := t . extractParentDir ( path )
if parentDir == "" || parentDir == "." {
return args , nil
}
log . Warnf ( "Deny read glob '%s' uses **; hiding parent directory '%s' to avoid expanding many bubblewrap arguments" , path , parentDir )
return t . processDenyReadRule ( parentDir )
}
paths , _ , err := t . expandGlobPattern ( path , t . config . mandatoryDenyScanDepth , t . config . maxGlobPaths )
if err != nil {
return args , nil
}
for _ , p := range paths {
if info , err := os . Stat ( p ); err == nil {
if info . IsDir () {
args = append ( args , "--tmpfs" , p )
} else {
args = append ( args , "--ro-bind" , "/dev/null" , p )
}
}
}
return args , nil
}
if info , err := os . Stat ( path ); err == nil {
if info . IsDir () {
args = append ( args , "--tmpfs" , path )
} else {
args = append ( args , "--ro-bind" , "/dev/null" , path )
}
} else if os . IsNotExist ( err ) {
log . Debugf ( "Deny read rule: skipping non-existent path '%s'" , path )
}
return args , nil
}
2026-05-11 16:12:06 +05:30
// processDenyWriteRule handles deny_write rules without masking reads. Files
// and directories are mounted read-only over any earlier writable parent bind.
func ( t * bubblewrapPolicyTranslator ) processDenyWriteRule ( path string ) ([] string , error ) {
args := [] string {}
if util . ContainsGlob ( path ) {
paths , _ , err := t . expandGlobPattern ( path , t . config . mandatoryDenyScanDepth , t . config . maxGlobPaths )
if err != nil {
return args , nil
}
for _ , p := range paths {
if _ , err := os . Stat ( p ); err == nil {
args = append ( args , "--ro-bind-try" , p , p )
log . Debugf ( "Deny write rule: mounted '%s' as read-only" , p )
}
}
return args , nil
}
if _ , err := os . Stat ( path ); err == nil {
args = append ( args , "--ro-bind-try" , path , path )
log . Debugf ( "Deny write rule: mounted '%s' as read-only" , path )
} else if os . IsNotExist ( err ) {
log . Debugf ( "Deny write rule: skipping non-existent path '%s'" , path )
}
return args , nil
}
2026-01-15 20:12:12 +05:30
// processReadRule handles a single allow_read rule, expanding globs and creating ro-bind mounts.
func ( t * bubblewrapPolicyTranslator ) processReadRule ( path string , boundPaths map [ string ] bool ) ([] string , error ) {
args := [] string {}
// Check if path contains glob pattern
if util . ContainsGlob ( path ) {
// Check if the base directory is already bound
baseDir := t . extractParentDir ( path )
if boundPaths [ baseDir ] {
log . Debugf ( "Skipping pattern '%s' - base directory '%s' already bound" , path , baseDir )
return args , nil
}
// Expand glob pattern to concrete paths with fallback detection
paths , useFallback , err := t . expandGlobPattern ( path , t . config . maxGlobDepth , t . config . maxGlobPaths )
if err != nil {
return nil , fmt . Errorf ( "failed to expand glob pattern: %w" , err )
}
if useFallback {
// Coarse-grained: bind parent directory
for _ , parentDir := range paths {
if ! boundPaths [ parentDir ] {
args = append ( args , "--ro-bind-try" , parentDir , parentDir )
boundPaths [ parentDir ] = true
log . Debugf ( "Coarse-grained fallback: bound parent directory '%s' (read-only)" , parentDir )
}
}
} else {
// Fine-grained: bind individual paths
for _ , p := range paths {
if ! boundPaths [ p ] {
args = append ( args , "--ro-bind-try" , p , p )
boundPaths [ p ] = true
}
}
}
} else {
// Literal path - create read-only bind
if ! boundPaths [ path ] {
args = append ( args , "--ro-bind-try" , path , path )
boundPaths [ path ] = true
}
}
return args , nil
}
// processWriteRule handles a single allow_write rule, expanding globs and creating rw-bind mounts.
func ( t * bubblewrapPolicyTranslator ) processWriteRule ( path string , boundPaths map [ string ] bool ) ([] string , error ) {
args := [] string {}
// Check if path contains glob pattern
if util . ContainsGlob ( path ) {
baseDir := t . extractParentDir ( path )
2026-06-07 10:03:23 +05:30
// 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.
// Base dir is the path prefix before the first "/**" (see extractGlobstarWriteBaseDir).
if strings . Contains ( path , "**" ) {
baseDir = extractGlobstarWriteBaseDir ( 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/**)
2026-01-15 20:12:12 +05:30
if boundPaths [ baseDir ] {
log . Debugf ( "Skipping pattern '%s' - base directory '%s' already bound" , path , baseDir )
return args , nil
}
// Expand glob pattern to concrete paths with fallback detection
paths , useFallback , err := t . expandGlobPattern ( path , t . config . maxGlobDepth , t . config . maxGlobPaths )
if err != nil {
return nil , fmt . Errorf ( "failed to expand glob pattern: %w" , err )
}
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 {
2026-06-07 10:03:23 +05:30
log . Debugf ( "Parent directory '%s' already bound for write, skipping duplicate bind" , parentDir )
2026-01-15 20:12:12 +05:30
}
}
} else {
// Fine-grained: bind individual paths
for _ , p := range paths {
// Check if path exists - if not, bind parent directory instead
// This allows creating new directories (e.g., node_modules/** when node_modules doesn't exist)
pathToBind := p
if _ , err := os . Stat ( p ); os . IsNotExist ( err ) {
parentDir := filepath . Dir ( p )
if parentDir != "" && parentDir != "." && parentDir != "/" {
pathToBind = parentDir
log . Debugf ( "Path '%s' doesn't exist, binding parent '%s' as writable to allow creation" , p , parentDir )
}
}
if ! boundPaths [ pathToBind ] {
args = append ( args , "--bind-try" , pathToBind , pathToBind )
boundPaths [ pathToBind ] = true
} else {
// Path already bound, add another bind to upgrade to read-write
// bwrap: later mounts override earlier ones
args = append ( args , "--bind-try" , pathToBind , pathToBind )
log . Debugf ( "Path '%s' already bound, adding write bind to override" , pathToBind )
}
}
}
} else {
// Literal path - create read-write bind
if ! boundPaths [ path ] {
args = append ( args , "--bind-try" , path , path )
boundPaths [ path ] = true
}
}
return args , nil
}
// processDenyRule handles deny rules by mounting /dev/null to prevent file access.
// This technique is borrowed from Anthropic's sandbox-runtime.
func ( t * bubblewrapPolicyTranslator ) processDenyRule ( path string ) ([] string , error ) {
args := [] string {}
// For glob patterns, expand and deny each path
if util . ContainsGlob ( path ) {
// For deny rules, we scan for existing files matching the pattern
// Note: For deny rules, we ignore the fallback indicator since we want to
// deny all matched paths individually for maximum security
paths , _ , err := t . expandGlobPattern ( path , t . config . mandatoryDenyScanDepth , t . config . maxGlobPaths )
if err != nil {
// If glob expansion fails, it's not critical for deny rules
return args , nil
}
for _ , p := range paths {
info , err := os . Stat ( p )
if err == nil {
if info . IsDir () {
// For directories, mount as read-only to prevent writes
// This overrides any previous writable bind of parent directories
args = append ( args , "--ro-bind-try" , p , p )
log . Debugf ( "Deny rule: mounted directory '%s' as read-only" , p )
} else {
// For files, mount /dev/null to prevent access
args = append ( args , "--ro-bind" , "/dev/null" , p )
}
}
}
} else {
// For literal paths, check if they exist
if info , err := os . Stat ( path ); err == nil {
if info . IsDir () {
// For directories, mount as read-only to prevent writes
// This overrides any previous writable bind of parent directories
args = append ( args , "--ro-bind-try" , path , path )
log . Debugf ( "Deny rule: mounted directory '%s' as read-only" , path )
} else {
// File exists - mount /dev/null over it
args = append ( args , "--ro-bind" , "/dev/null" , path )
}
} else if os . IsNotExist ( err ) {
// File doesn't exist - skip it
// IMPORTANT: We cannot use --ro-bind /dev/null for non-existent paths because
// bwrap creates the file on the host filesystem as a mount point, which leaves
// empty files (.env, .aws, etc.) in the user's directory after sandbox exits.
// Non-existent files are harmless (no secrets to leak), and blocking creation
// in writable directories isn't critical since an attacker creating an empty
// .env is not a security threat.
log . Debugf ( "Deny rule: skipping non-existent path '%s' (bwrap would create empty file as mount point)" , path )
}
}
return args , nil
}
// expandGlobPattern expands a glob pattern to a list of concrete paths.
// Implements depth limiting and path count limiting to prevent DoS.
// Returns (paths, useFallback, error) where useFallback indicates if
// coarse-grained parent directory fallback should be used.
func ( t * bubblewrapPolicyTranslator ) expandGlobPattern ( pattern string , maxDepth int , maxPaths int ) ([] string , bool , error ) {
// Handle ** globstar patterns specially
if strings . Contains ( pattern , "**" ) {
paths , err := t . expandGlobstarPattern ( pattern , maxDepth , maxPaths )
if err != nil {
return nil , false , err
}
// Check if we should use fallback
if len ( paths ) > t . config . globFallbackThreshold {
log . Warnf ( "Glob pattern '%s' matched %d paths (threshold: %d), using coarse-grained parent directory fallback for scalability" ,
pattern , len ( paths ), t . config . globFallbackThreshold )
parentDir := t . extractParentDir ( pattern )
return [] string { parentDir }, true , nil
}
return paths , false , nil
}
// Use filepath.Glob for simple patterns (*, ?, [])
matches , err := filepath . Glob ( pattern )
if err != nil {
return nil , false , fmt . Errorf ( "glob expansion failed: %w" , err )
}
// Check fallback threshold before applying maxPaths limit
if len ( matches ) > t . config . globFallbackThreshold {
log . Warnf ( "Glob pattern '%s' matched %d paths (threshold: %d), using coarse-grained parent directory fallback for scalability" ,
pattern , len ( matches ), t . config . globFallbackThreshold )
parentDir := t . extractParentDir ( pattern )
return [] string { parentDir }, true , nil
}
// Limit number of matches (shouldn't happen if fallback threshold < maxPaths)
if len ( matches ) > maxPaths {
log . Warnf ( "Glob pattern '%s' matched %d paths, limiting to %d" , pattern , len ( matches ), maxPaths )
matches = matches [: maxPaths ]
}
return matches , false , nil
}
2026-05-07 12:42:28 +05:30
func ( t * bubblewrapPolicyTranslator ) expandGlobstarPattern ( pattern string , maxDepth , maxPaths int ) ([] string , error ) {
return expandGlobstarPattern ( pattern , maxDepth , maxPaths )
2026-01-15 20:12:12 +05:30
}
func ( t * bubblewrapPolicyTranslator ) extractParentDir ( pattern string ) string {
2026-05-07 12:42:28 +05:30
return extractGlobParentDir ( pattern )
2026-01-15 20:12:12 +05:30
}
// addPTYSupport adds arguments for pseudo-terminal support.
// Required for interactive package manager commands.
func ( t * bubblewrapPolicyTranslator ) addPTYSupport () [] string {
args := [] string {}
// Bind /dev/pts for PTY allocation
args = append ( args , "--dev-bind-try" , "/dev/pts" , "/dev/pts" )
// Bind /dev/ptmx for PTY master
args = append ( args , "--dev-bind-try" , "/dev/ptmx" , "/dev/ptmx" )
return args
}
// addTmpdirSupport adds arguments for temporary directory access.
// Package managers need writable temp space for downloads, extraction, etc.
func ( t * bubblewrapPolicyTranslator ) addTmpdirSupport () [] string {
args := [] string {}
tmpDir := os . TempDir ()
// Bind tmp directory as writable
// Use --bind instead of --bind-try to ensure it's available
args = append ( args , "--bind" , tmpDir , tmpDir )
return args
}
2026-05-06 12:45:36 +05:30
func expandAll ( patterns [] string ) ([] string , error ) {
out := make ([] string , 0 , len ( patterns ))
for _ , p := range patterns {
expanded , err := util . ExpandVariables ( p )
if err != nil {
return nil , fmt . Errorf ( "failed to expand pattern %q: %w" , p , err )
}
out = append ( out , expanded )
}
return out , nil
}
// intersectStrings returns the order-preserving intersection of a and b.
func intersectStrings ( a , b [] string ) [] string {
bset := make ( map [ string ] bool , len ( b ))
for _ , x := range b {
bset [ x ] = true
}
out := [] string {}
for _ , x := range a {
if bset [ x ] {
out = append ( out , x )
}
}
return out
}