summaryrefslogtreecommitdiff
path: root/internal/namespace/namespace.go
diff options
context:
space:
mode:
authorJames O'Doherty <james@theodohertyfamily.com>2026-05-22 10:46:02 -0400
committerJames O'Doherty <james@theodohertyfamily.com>2026-05-22 10:46:02 -0400
commit9131b0004e7c640cc028179e1d049a4c62210d94 (patch)
tree7efb5612b61240105851cb5d8ac8f05263644db4 /internal/namespace/namespace.go
parent401683a6b11e5a7810c949147a12f2c4bbfba48a (diff)
Security hardening: prevent shell injection and null-byte crashes, implement 8-bit clean argument fuzzing and portable E2E binary discovery
Diffstat (limited to 'internal/namespace/namespace.go')
-rw-r--r--internal/namespace/namespace.go41
1 files changed, 29 insertions, 12 deletions
diff --git a/internal/namespace/namespace.go b/internal/namespace/namespace.go
index 1a09b78..b0794a4 100644
--- a/internal/namespace/namespace.go
+++ b/internal/namespace/namespace.go
@@ -2,7 +2,6 @@ package namespace
import (
_ "embed"
- "encoding/json"
"fmt"
"os"
"os/exec"
@@ -55,15 +54,13 @@ func VerifyIsolation() (bool, string) {
return true, "Isolated and root"
}
-// VerifyArguments prints the current process arguments as a JSON array.
-// This is used for E2E testing to verify that argument splitting and
-// shell injection are not occurring during the bootstrap loop.
+// VerifyArguments prints the current process arguments as hex-encoded strings.
+// This is used for E2E testing to verify that the data path is 8-bit clean
+// and that no bytes are mutated during the bootstrap loop.
func VerifyArguments(args []string) error {
- out, err := json.Marshal(args)
- if err != nil {
- return fmt.Errorf("failed to marshal arguments: %w", err)
+ for i, arg := range args {
+ fmt.Printf("%d:%x\n", i, arg)
}
- fmt.Println(string(out))
return nil
}
@@ -74,6 +71,16 @@ func Bootstrap() error {
return nil
}
+ // 0. Validate current arguments for null bytes before proceeding.
+ // If any argument contains a null byte, syscall.Exec will fail with 'invalid argument'.
+ for i, arg := range os.Args {
+ for j := 0; j < len(arg); j++ {
+ if arg[j] == 0 {
+ return fmt.Errorf("argument %d contains null byte at position %d", i, j)
+ }
+ }
+ }
+
self, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
@@ -89,16 +96,16 @@ func Bootstrap() error {
// 2. Write the embedded launcher binary to the temp file.
if _, err := tmpFile.Write(launcherBytes); err != nil {
- tmpFile.Close()
+ _ = tmpFile.Close()
return fmt.Errorf("failed to write launcher binary: %w", err)
}
-
+
// Ensure the binary is executable (0700)
if err := tmpFile.Chmod(0700); err != nil {
- tmpFile.Close()
+ _ = tmpFile.Close()
return fmt.Errorf("failed to set launcher permissions: %w", err)
}
- tmpFile.Close()
+ _ = tmpFile.Close()
// 3. Prepare arguments for the launcher.
// The launcher expects: launcher <command_to_run> [args...]
@@ -106,6 +113,16 @@ func Bootstrap() error {
args = append(args, os.Args[1:]...)
// 4. Replace the current process with the launcher.
+ // We must check for null bytes in the arguments here because syscall.Exec
+ // (which calls execve) will return 'invalid argument' (EINVAL) if any
+ // string in the argv array contains a null byte.
+ for i, arg := range args {
+ for j := 0; j < len(arg); j++ {
+ if arg[j] == 0 {
+ return fmt.Errorf("launcher argument %d contains null byte at position %d", i, j)
+ }
+ }
+ }
err = syscall.Exec(launcherPath, args, os.Environ())
if err != nil {
return fmt.Errorf("launcher exec failed: %w", err)