Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changes/unreleased/+thread-local-sandbox-verification.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
kind: Fixed
body: Verify application sandbox capabilities on the exact Linux thread that will launch the workload.
2 changes: 1 addition & 1 deletion docs/CONTROLLED_SESSION_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,7 @@ in a final runtime layer, creates the locked container-local account there,
records that layer outside the provider graph, and uses its fixed
sandbox-and-exec contract as the outermost process for persistent
workloads, transient commands, shells, and lifecycle commands. The verifier
fails closed unless `/proc/self/status` reports seccomp filtering,
fails closed unless `/proc/thread-self/status` reports seccomp filtering,
`no-new-privileges`, and empty inheritable, effective, permitted, bounding, and
ambient capability
sets, then directly executes the exact application argv. Private-environment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestPrivateWorkloadEnvironmentDockerIntegrationMasksFilesAndInjectsValues(t
if os.Getenv("REPLOY_DOCKER_INTEGRATION") != "1" {
t.Skip("set REPLOY_DOCKER_INTEGRATION=1 to run Docker integration evidence")
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
defer cancel()

image, _ := buildApplicationStartupVerifierIntegrationImage(t, ctx)
Expand Down Expand Up @@ -152,7 +152,7 @@ func TestPrivateRuntimeMasksDockerIntegrationProtectTransientContainer(t *testin
if os.Getenv("REPLOY_DOCKER_INTEGRATION") != "1" {
t.Skip("set REPLOY_DOCKER_INTEGRATION=1 to run Docker integration evidence")
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
defer cancel()

image, _ := buildApplicationStartupVerifierIntegrationImage(t, ctx)
Expand Down
6 changes: 5 additions & 1 deletion internal/probe/startup_verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import (
"strings"
)

const applicationKernelStatusPath = "/proc/self/status"
// Sandbox setup is deliberately pinned to one OS thread because Linux
// credentials and capability sets are thread-scoped. /proc/self/status
// describes the thread-group leader, which may be a different Go runtime
// thread; verify the exact thread that will exec the application instead.
const applicationKernelStatusPath = "/proc/thread-self/status"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve support for kernels without thread-self

On Linux kernels before 3.17, /proc/thread-self does not exist, so every sandboxed persistent or transient workload now fails closed while reading this path instead of launching. The repository supports Linux Docker Engine without declaring a newer kernel floor, and the previous /proc/self/status path worked on these systems; use the current TID via /proc/self/task/<tid>/status as a compatibility fallback while retaining thread-local verification.

Useful? React with 👍 / 👎.

Comment on lines +12 to +16
Comment on lines +12 to +16

var requiredApplicationKernelStatusV1 = []struct {
name string
Expand Down
68 changes: 68 additions & 0 deletions internal/probe/startup_verifier_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//go:build linux

package probe

import (
"os"
"runtime"
"strconv"
"strings"
"testing"

"golang.org/x/sys/unix"
)

func TestReadApplicationKernelStatusUsesCallingThread(t *testing.T) {
type result struct {
tid int
content []byte
err error
}
results := make(chan result, 2)
release := make(chan struct{})
done := make(chan struct{}, 2)
defer func() {
close(release)
for range 2 {
<-done
}
}()
for range 2 {
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
defer func() { done <- struct{}{} }()
content, err := readApplicationKernelStatus()
results <- result{tid: unix.Gettid(), content: content, err: err}
<-release
}()
}

observedNonLeader := false
for range 2 {
result := <-results
if result.err != nil {
t.Fatal(result.err)
}
if result.tid != os.Getpid() {
observedNonLeader = true
}
var statusPID string
for _, line := range strings.Split(string(result.content), "\n") {
name, raw, found := strings.Cut(line, ":")
if found && name == "Pid" {
fields := strings.Fields(raw)
if len(fields) == 1 {
statusPID = fields[0]
}
break
}
}
if statusPID != strconv.Itoa(result.tid) {
t.Fatalf("status Pid = %q, want calling thread %d", statusPID, result.tid)
}
}
if !observedNonLeader {
t.Fatal("test did not observe a non-leader OS thread")
}
}
Loading