Skip to content

Commit 60ffe20

Browse files
blink-so[bot]f0ssel
andcommitted
feat: preserve original user environment when running under sudo
When jail is executed with sudo, the subprocess now receives the original user's environment variables (USER, HOME, PATH, XDG directories) instead of root's environment. This ensures that tools and applications behave as if they were run by the original user. Changes: - Add environment package with sudo detection and user environment restoration - Update Linux jail implementation to restore user environment - Update macOS jail implementation to restore user environment - Preserve important variables: USER, LOGNAME, HOME, PATH, XDG_* directories - Maintain backward compatibility for non-sudo execution Co-authored-by: f0ssel <[email protected]>
1 parent 31f1722 commit 60ffe20

File tree

3 files changed

+166
-1
lines changed

3 files changed

+166
-1
lines changed

environment/sudo.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package environment
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"os"
7+
"os/user"
8+
"path/filepath"
9+
"strconv"
10+
"strings"
11+
)
12+
13+
// RestoreOriginalUserEnvironment detects if running under sudo and restores
14+
// the original user's environment variables that are important for subprocess execution.
15+
func RestoreOriginalUserEnvironment(logger *slog.Logger) map[string]string {
16+
restoredEnv := make(map[string]string)
17+
18+
// Check if running under sudo by looking for SUDO_USER
19+
sudoUser := os.Getenv("SUDO_USER")
20+
if sudoUser == "" {
21+
logger.Debug("Not running under sudo, no environment restoration needed")
22+
return restoredEnv
23+
}
24+
25+
logger.Debug("Detected sudo execution, restoring original user environment", "sudo_user", sudoUser)
26+
27+
// Get original user information
28+
originalUser, err := user.Lookup(sudoUser)
29+
if err != nil {
30+
logger.Warn("Failed to lookup original user, skipping environment restoration", "sudo_user", sudoUser, "error", err)
31+
return restoredEnv
32+
}
33+
34+
// Restore basic user identity variables
35+
restoredEnv["USER"] = sudoUser
36+
restoredEnv["LOGNAME"] = sudoUser
37+
restoredEnv["HOME"] = originalUser.HomeDir
38+
39+
// Restore original user's UID and GID if available
40+
if sudoUID := os.Getenv("SUDO_UID"); sudoUID != "" {
41+
restoredEnv["UID"] = sudoUID
42+
}
43+
if sudoGID := os.Getenv("SUDO_GID"); sudoGID != "" {
44+
restoredEnv["GID"] = sudoGID
45+
}
46+
47+
// Try to restore a reasonable PATH for the original user
48+
// This is a best-effort attempt since the original PATH might be complex
49+
restoredPath := restoreUserPath(originalUser, logger)
50+
if restoredPath != "" {
51+
restoredEnv["PATH"] = restoredPath
52+
}
53+
54+
// Restore XDG directories for the original user
55+
restoreXDGEnvironment(originalUser, restoredEnv)
56+
57+
// Log what we're restoring
58+
logger.Debug("Restored environment variables for original user",
59+
"user", sudoUser,
60+
"home", originalUser.HomeDir,
61+
"restored_vars", len(restoredEnv))
62+
63+
return restoredEnv
64+
}
65+
66+
// restoreUserPath attempts to construct a reasonable PATH for the original user
67+
func restoreUserPath(originalUser *user.User, logger *slog.Logger) string {
68+
// Start with common system paths
69+
systemPaths := []string{
70+
"/usr/local/bin",
71+
"/usr/bin",
72+
"/bin",
73+
}
74+
75+
// Add user-specific paths
76+
userPaths := []string{
77+
filepath.Join(originalUser.HomeDir, ".local", "bin"),
78+
filepath.Join(originalUser.HomeDir, "bin"),
79+
}
80+
81+
// Check if user paths exist and add them
82+
var validPaths []string
83+
for _, path := range userPaths {
84+
if _, err := os.Stat(path); err == nil {
85+
validPaths = append(validPaths, path)
86+
}
87+
}
88+
89+
// Combine user paths + system paths
90+
allPaths := append(validPaths, systemPaths...)
91+
92+
// Also try to preserve some paths from current PATH that might be user-specific
93+
currentPath := os.Getenv("PATH")
94+
if currentPath != "" {
95+
for _, path := range strings.Split(currentPath, ":") {
96+
// Include paths that contain the user's home directory
97+
if strings.Contains(path, originalUser.HomeDir) {
98+
if _, err := os.Stat(path); err == nil {
99+
allPaths = append([]string{path}, allPaths...)
100+
}
101+
}
102+
}
103+
}
104+
105+
restoredPath := strings.Join(allPaths, ":")
106+
logger.Debug("Restored PATH for user", "user", originalUser.Username, "path", restoredPath)
107+
return restoredPath
108+
}
109+
110+
// restoreXDGEnvironment restores XDG Base Directory variables for the original user
111+
func restoreXDGEnvironment(originalUser *user.User, restoredEnv map[string]string) {
112+
homeDir := originalUser.HomeDir
113+
114+
// Set XDG directories according to the specification
115+
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
116+
restoredEnv["XDG_DATA_HOME"] = filepath.Join(homeDir, ".local", "share")
117+
restoredEnv["XDG_CONFIG_HOME"] = filepath.Join(homeDir, ".config")
118+
restoredEnv["XDG_STATE_HOME"] = filepath.Join(homeDir, ".local", "state")
119+
restoredEnv["XDG_CACHE_HOME"] = filepath.Join(homeDir, ".cache")
120+
121+
// XDG_RUNTIME_DIR is typically /run/user/{uid} but we'll leave it as-is
122+
// since it requires the actual UID and proper permissions
123+
}
124+
125+
// GetEffectiveUID returns the effective UID that should be used for the subprocess
126+
// This helps determine if we need to drop privileges when running under sudo
127+
func GetEffectiveUID() (int, error) {
128+
if sudoUID := os.Getenv("SUDO_UID"); sudoUID != "" {
129+
uid, err := strconv.Atoi(sudoUID)
130+
if err != nil {
131+
return 0, fmt.Errorf("invalid SUDO_UID: %v", err)
132+
}
133+
return uid, nil
134+
}
135+
return os.Getuid(), nil
136+
}
137+
138+
// GetEffectiveGID returns the effective GID that should be used for the subprocess
139+
func GetEffectiveGID() (int, error) {
140+
if sudoGID := os.Getenv("SUDO_GID"); sudoGID != "" {
141+
gid, err := strconv.Atoi(sudoGID)
142+
if err != nil {
143+
return 0, fmt.Errorf("invalid SUDO_GID: %v", err)
144+
}
145+
return gid, nil
146+
}
147+
return os.Getgid(), nil
148+
}

network/linux.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"os/exec"
1010
"syscall"
1111
"time"
12+
13+
"github.com/coder/jail/environment"
1214
)
1315

1416
const (
@@ -92,6 +94,12 @@ func (l *LinuxJail) Execute(command []string, extraEnv map[string]string) error
9294
l.logger.Debug("Setting up environment")
9395
env := os.Environ()
9496

97+
// Restore original user environment if running under sudo
98+
restoredUserEnv := environment.RestoreOriginalUserEnvironment(l.logger)
99+
for key, value := range restoredUserEnv {
100+
env = append(env, fmt.Sprintf("%s=%s", key, value))
101+
}
102+
95103
// Add extra environment variables (including CA cert if provided)
96104
for key, value := range extraEnv {
97105
env = append(env, fmt.Sprintf("%s=%s", key, value))

network/macos.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import (
1010
"strconv"
1111
"strings"
1212
"syscall"
13+
"time"
14+
15+
"github.com/coder/jail/environment"
1316
)
1417

1518
const (
@@ -79,6 +82,12 @@ func (m *MacOSNetJail) Execute(command []string, extraEnv map[string]string) err
7982
m.logger.Debug("Setting up environment")
8083
env := os.Environ()
8184

85+
// Restore original user environment if running under sudo
86+
restoredUserEnv := environment.RestoreOriginalUserEnvironment(m.logger)
87+
for key, value := range restoredUserEnv {
88+
env = append(env, fmt.Sprintf("%s=%s", key, value))
89+
}
90+
8291
// Add extra environment variables (including CA cert if provided)
8392
for key, value := range extraEnv {
8493
env = append(env, fmt.Sprintf("%s=%s", key, value))
@@ -334,4 +343,4 @@ func (m *MacOSNetJail) cleanupTempFiles() {
334343
if m.mainRulesPath != "" {
335344
os.Remove(m.mainRulesPath)
336345
}
337-
}
346+
}

0 commit comments

Comments
 (0)