From 1449e8760804014f62142b1bf28c07d7b753b430 Mon Sep 17 00:00:00 2001 From: Andrzej J Skalski Date: Wed, 15 Apr 2026 14:51:08 +0200 Subject: [PATCH 1/4] feat: minimal sandbox support in remote execution --- src/remote/action.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/remote/action.go b/src/remote/action.go index 2098ec48c3..267b702f11 100644 --- a/src/remote/action.go +++ b/src/remote/action.go @@ -128,7 +128,7 @@ func (c *Client) buildCommand(target *core.BuildTarget, inputRoot *pb.Directory, cmd, err := core.ReplaceSequences(state, target, cmd) return &pb.Command{ Platform: c.targetPlatformProperties(target), - Arguments: process.BashCommand(c.shellPath, commandPrefixBuilder.String()+cmd, state.Config.Build.ExitOnError), + Arguments: c.sandboxArgs(target.Sandbox, process.BashCommand(c.shellPath, commandPrefixBuilder.String()+cmd, state.Config.Build.ExitOnError)), EnvironmentVariables: c.buildEnv(target, c.stampedBuildEnvironment(state, target, inputRoot, stamp, isTest || isRun), target.Sandbox), OutputPaths: outs, }, err @@ -168,7 +168,7 @@ func (c *Client) buildTestCommand(state *core.BuildState, target *core.BuildTarg }, }, }, - Arguments: process.BashCommand(c.shellPath, commandPrefix+cmd, state.Config.Build.ExitOnError), + Arguments: c.sandboxArgs(target.Test.Sandbox, process.BashCommand(c.shellPath, commandPrefix+cmd, state.Config.Build.ExitOnError)), EnvironmentVariables: c.buildEnv(nil, core.TestEnvironment(state, target, ".", run), target.Test.Sandbox), OutputPaths: paths, }, err @@ -573,10 +573,31 @@ func reallyTranslateOS(os string) string { } } +// sandboxArgs prepends the configured external sandbox tool to the given argument list, +// matching what local execution does in exec_linux.go for the non-plz-sandbox case. +// Returns args unchanged if sandboxing is disabled or no external tool is configured. +func (c *Client) sandboxArgs(sandbox bool, args []string) []string { + if !sandbox { + return args + } + tool := c.state.Config.Sandbox.Tool + if tool == "" { + // Built-in plz sandbox re-execs into the local plz binary; not supported remotely. + return args + } + return append([]string{tool}, args...) +} + // buildEnv translates the set of environment variables for this target to a proto. func (c *Client) buildEnv(target *core.BuildTarget, env core.BuildEnv, sandbox bool) []*pb.Command_EnvironmentVariable { if sandbox { env["SANDBOX"] = "true" + if c.state.Config.Sandbox.Tool != "" { + // Mirror what local execution sets so the sandbox tool sees the same interface. + // SHARE_NETWORK/SHARE_MOUNT=0 means "don't share" i.e. sandbox that namespace. + env["SHARE_NETWORK"] = "0" + env["SHARE_MOUNT"] = "0" + } } if target != nil && target.IsBinary { env["_BINARY"] = "true" From 60b6355172ccd8c0649f3a322a266f8e780e1aa1 Mon Sep 17 00:00:00 2001 From: Andrzej J Skalski Date: Wed, 15 Apr 2026 20:47:44 +0200 Subject: [PATCH 2/4] fix: don't guess env vars --- src/remote/action.go | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/remote/action.go b/src/remote/action.go index 267b702f11..632ea2dada 100644 --- a/src/remote/action.go +++ b/src/remote/action.go @@ -117,7 +117,7 @@ func (c *Client) buildCommand(target *core.BuildTarget, inputRoot *pb.Directory, Arguments: []string{ "fetch", strings.Join(target.AllURLs(state), " "), "verify", strings.Join(target.Hashes, " "), }, - EnvironmentVariables: c.buildEnv(target, map[string]string{}, false), + EnvironmentVariables: c.buildEnv(target, map[string]string{}, process.NoSandbox), OutputPaths: outs, }, nil } @@ -129,7 +129,7 @@ func (c *Client) buildCommand(target *core.BuildTarget, inputRoot *pb.Directory, return &pb.Command{ Platform: c.targetPlatformProperties(target), Arguments: c.sandboxArgs(target.Sandbox, process.BashCommand(c.shellPath, commandPrefixBuilder.String()+cmd, state.Config.Build.ExitOnError)), - EnvironmentVariables: c.buildEnv(target, c.stampedBuildEnvironment(state, target, inputRoot, stamp, isTest || isRun), target.Sandbox), + EnvironmentVariables: c.buildEnv(target, c.stampedBuildEnvironment(state, target, inputRoot, stamp, isTest || isRun), process.NewSandboxConfig(target.Sandbox, target.Sandbox)), OutputPaths: outs, }, err } @@ -169,7 +169,7 @@ func (c *Client) buildTestCommand(state *core.BuildState, target *core.BuildTarg }, }, Arguments: c.sandboxArgs(target.Test.Sandbox, process.BashCommand(c.shellPath, commandPrefix+cmd, state.Config.Build.ExitOnError)), - EnvironmentVariables: c.buildEnv(nil, core.TestEnvironment(state, target, ".", run), target.Test.Sandbox), + EnvironmentVariables: c.buildEnv(nil, core.TestEnvironment(state, target, ".", run), process.NewSandboxConfig(target.Test.Sandbox, target.Test.Sandbox)), OutputPaths: paths, }, err } @@ -183,7 +183,7 @@ func (c *Client) buildRunCommand(state *core.BuildState, target *core.BuildTarge return &pb.Command{ Platform: c.platform, Arguments: outs, - EnvironmentVariables: c.buildEnv(target, core.GeneralBuildEnvironment(state), false), + EnvironmentVariables: c.buildEnv(target, core.GeneralBuildEnvironment(state), process.NoSandbox), }, nil } @@ -589,14 +589,20 @@ func (c *Client) sandboxArgs(sandbox bool, args []string) []string { } // buildEnv translates the set of environment variables for this target to a proto. -func (c *Client) buildEnv(target *core.BuildTarget, env core.BuildEnv, sandbox bool) []*pb.Command_EnvironmentVariable { - if sandbox { +func (c *Client) buildEnv(target *core.BuildTarget, env core.BuildEnv, sandbox process.SandboxConfig) []*pb.Command_EnvironmentVariable { + if sandbox != process.NoSandbox { env["SANDBOX"] = "true" if c.state.Config.Sandbox.Tool != "" { - // Mirror what local execution sets so the sandbox tool sees the same interface. - // SHARE_NETWORK/SHARE_MOUNT=0 means "don't share" i.e. sandbox that namespace. - env["SHARE_NETWORK"] = "0" - env["SHARE_MOUNT"] = "0" + shareNetwork := "1" + if sandbox.Network { + shareNetwork = "0" + } + shareMount := "1" + if sandbox.Mount { + shareMount = "0" + } + env["SHARE_NETWORK"] = shareNetwork + env["SHARE_MOUNT"] = shareMount } } if target != nil && target.IsBinary { From c9dd514808c176c00a4a1dd56554bb0482a798d7 Mon Sep 17 00:00:00 2001 From: Andrzej J Skalski Date: Tue, 12 May 2026 17:17:18 +0200 Subject: [PATCH 3/4] remote sandbox disabled, unless specifically requested via config --- src/core/config.go | 1 + src/remote/action.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/config.go b/src/core/config.go index a54d97b6ca..0bfb6cb15a 100644 --- a/src/core/config.go +++ b/src/core/config.go @@ -559,6 +559,7 @@ type Configuration struct { Namespace string `help:"Set to 'always', to namespace all actions. Set to 'sandbox' to namespace only when sandboxing the build action. Defaults to 'never', under the assumption the sandbox tool will handle its own namespacing. If set, user namespacing will be enabled for all rules. Mount and network will only be enabled if the rule is to be sandboxed."` Build bool `help:"True to sandbox individual build actions, which isolates them from network access and some aspects of the filesystem. Currently only works on Linux." var:"BUILD_SANDBOX"` Test bool `help:"True to sandbox individual tests, which isolates them from network access, IPC and some aspects of the filesystem. Currently only works on Linux." var:"TEST_SANDBOX"` + Remote bool `help:"True to enable sandboxing for remotely executed actions. Requires an external sandbox tool to be configured via the Tool option." var:"REMOTE_SANDBOX"` ExcludeableTargets []BuildLabel `help:"If set, only targets that match these wildcards will be allowed to opt out of the sandbox"` } `help:"A config section describing settings relating to sandboxing of build actions."` Remote struct { diff --git a/src/remote/action.go b/src/remote/action.go index 632ea2dada..fadcd9d68b 100644 --- a/src/remote/action.go +++ b/src/remote/action.go @@ -577,7 +577,7 @@ func reallyTranslateOS(os string) string { // matching what local execution does in exec_linux.go for the non-plz-sandbox case. // Returns args unchanged if sandboxing is disabled or no external tool is configured. func (c *Client) sandboxArgs(sandbox bool, args []string) []string { - if !sandbox { + if !sandbox || !c.state.Config.Sandbox.Remote { return args } tool := c.state.Config.Sandbox.Tool @@ -590,7 +590,7 @@ func (c *Client) sandboxArgs(sandbox bool, args []string) []string { // buildEnv translates the set of environment variables for this target to a proto. func (c *Client) buildEnv(target *core.BuildTarget, env core.BuildEnv, sandbox process.SandboxConfig) []*pb.Command_EnvironmentVariable { - if sandbox != process.NoSandbox { + if sandbox != process.NoSandbox && c.state.Config.Sandbox.Remote { env["SANDBOX"] = "true" if c.state.Config.Sandbox.Tool != "" { shareNetwork := "1" From b319c7c89c0a5f1aef1cf5dc514ada02767c3249 Mon Sep 17 00:00:00 2001 From: Andrzej J Skalski Date: Fri, 15 May 2026 13:48:38 +0200 Subject: [PATCH 4/4] docs: document sandbox.remote config option Adds the missing section in config.html for the new Sandbox.Remote field so that //docs/test:docs_test passes. --- docs/config.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/config.html b/docs/config.html index c7f8cb8b92..49408ee39d 100644 --- a/docs/config.html +++ b/docs/config.html @@ -884,6 +884,20 @@

+
  • +
    +

    + Remote (bool) +

    + +

    + Enables sandboxing for remotely executed actions. This requires an external + sandbox tool to be configured via the Tool + option, which the remote worker will invoke to wrap the action. Defaults to + False. +

    +
    +