fix(agentcompat): validate supervised executable paths

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
naiba
2026-07-20 17:02:21 +00:00
co-authored by naiba/CloudCode
parent 8a0382478b
commit c7799854b9
3 changed files with 74 additions and 20 deletions
@@ -6,7 +6,6 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"sync"
@@ -21,22 +20,6 @@ const (
Stderr Stream = "stderr"
)
type Spec struct {
Name string
Path string
Args []string
Dir string
Env []string
ExtraFiles []*os.File
Stdout io.Writer
Stderr io.Writer
MaxLogBytes int
TerminateTimeout time.Duration
KillTimeout time.Duration
Readiness func(Stream, string) bool
Credential *syscall.Credential
}
type Supervisor struct {
ctx context.Context
spec Spec
@@ -62,10 +45,10 @@ func NewSupervisor(ctx context.Context, spec Spec) *Supervisor {
}
func (supervisor *Supervisor) Start() error {
if supervisor.spec.Name == "" || supervisor.spec.Path == "" || supervisor.spec.MaxLogBytes < 1 || supervisor.spec.TerminateTimeout <= 0 || supervisor.spec.KillTimeout <= 0 {
return errors.New("invalid process specification")
if err := supervisor.spec.validate(); err != nil {
return err
}
command := exec.Command(supervisor.spec.Path, supervisor.spec.Args...)
command := exec.Command(supervisor.spec.Path, supervisor.spec.Args...) // #nosec G204 -- Absolute executable regular-file path is validated before fixed argv execution; no shell is invoked.
command.Dir = supervisor.spec.Dir
command.Env = supervisor.spec.Env
if command.Env == nil {
@@ -0,0 +1,46 @@
//go:build linux
package process
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"syscall"
"time"
)
type Spec struct {
Name string
Path string
Args []string
Dir string
Env []string
ExtraFiles []*os.File
Stdout io.Writer
Stderr io.Writer
MaxLogBytes int
TerminateTimeout time.Duration
KillTimeout time.Duration
Readiness func(Stream, string) bool
Credential *syscall.Credential
}
func (spec Spec) validate() error {
if spec.Name == "" || spec.Path == "" || spec.MaxLogBytes < 1 || spec.TerminateTimeout <= 0 || spec.KillTimeout <= 0 {
return errors.New("invalid process specification")
}
if !filepath.IsAbs(spec.Path) {
return errors.New("process path must be absolute")
}
info, err := os.Stat(spec.Path)
if err != nil {
return fmt.Errorf("stat process path: %w", err)
}
if !info.Mode().IsRegular() || info.Mode()&0o111 == 0 {
return errors.New("process path must be an executable regular file")
}
return nil
}
@@ -29,6 +29,31 @@ func TestSupervisor_CleanExit(t *testing.T) {
requireNoError(t, supervisor.Wait(t.Context()))
}
func TestSupervisor_StartRejectsUntrustedExecutablePaths(t *testing.T) {
tests := []struct {
name string
path string
}{
{name: "relative", path: "relative-helper"},
{name: "directory", path: t.TempDir()},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Given
supervisor := newHelperSupervisor(t.Context(), "clean", nil)
supervisor.spec.Path = test.path
// When
err := supervisor.Start()
// Then
if err == nil {
t.Fatal("untrusted process path was accepted")
}
})
}
}
func TestSupervisor_RunsChildWithConfiguredCredential(t *testing.T) {
// Given
credentialDirectory, err := os.MkdirTemp("/tmp", "agentcompat-credential-")