@@ -18,6 +18,7 @@ import (
1818// jailArgv0 is the internal subcommand the consumer's main routes to RunJail
1919// before any normal CLI parsing.
2020const jailArgv0 = "__jail"
21+ const jailStashPath = "/tmp/.cliguard-jail"
2122
2223// Wrap rewrites cmd to re-exec the consumer binary as the jail helper in a fresh
2324// user+mount namespace. No-op when jailed, opted out (EnvNoSandbox), or spec nil.
@@ -95,45 +96,132 @@ func RunJail(args []string) error {
9596 if execTool == "" {
9697 return fmt .Errorf ("sandbox: jail missing --exec target" )
9798 }
99+ resolvedTools := resolveTools (tools )
100+ if err := prepareJailMounts (resolvedTools ); err != nil {
101+ return err
102+ }
103+ if err := installToolShims (resolvedTools , spec ().SelfExe ); err != nil {
104+ return err
105+ }
106+ if err := setJailEnvironment (); err != nil {
107+ return err
108+ }
109+ if err := clearAmbientCaps (); err != nil {
110+ return err
111+ }
112+ if err := lockdownSyscalls (); err != nil {
113+ return err
114+ }
115+ return execJailTarget (execTool , rest )
116+ }
117+
118+ type resolvedTool struct {
119+ tool string
120+ canonical string
121+ realPath string
122+ dir string
123+ needsLink bool
124+ }
125+
126+ // resolveTools snapshots the wrapped tools' PATH entries before any mount
127+ // rewrite can hide them.
128+ func resolveTools (tools []string ) []resolvedTool {
129+ resolved := make ([]resolvedTool , 0 , len (tools ))
130+ for _ , tool := range tools {
131+ rt , ok := resolveTool (tool )
132+ if ok {
133+ resolved = append (resolved , rt )
134+ }
135+ }
136+ return resolved
137+ }
98138
139+ // resolveTool records one wrapped tool's current PATH entry. Missing tools stay
140+ // a no-op, matching the pre-refactor behavior.
141+ func resolveTool (tool string ) (resolvedTool , bool ) {
142+ canonical , err := exec .LookPath (tool )
143+ if err != nil {
144+ // Tool not present on this host: nothing to shim.
145+ return resolvedTool {}, false
146+ }
147+ realPath := canonical
148+ if resolved , err := filepath .EvalSymlinks (canonical ); err == nil {
149+ realPath = resolved
150+ }
151+ return resolvedTool {
152+ tool : tool ,
153+ canonical : canonical ,
154+ realPath : realPath ,
155+ dir : filepath .Dir (canonical ),
156+ needsLink : realPath != canonical ,
157+ }, true
158+ }
159+
160+ // prepareJailMounts makes the mount namespace private and stages the real tools
161+ // inside a private tmpfs before canonical dirs are masked.
162+ func prepareJailMounts (resolvedTools []resolvedTool ) error {
99163 // Make our mount view private so binds never propagate back to the host.
100164 if err := unix .Mount ("" , "/" , "" , unix .MS_REC | unix .MS_PRIVATE , "" ); err != nil {
101165 return fmt .Errorf ("sandbox: make-rprivate: %w" , err )
102166 }
103167
104168 // A private tmpfs stashes real binaries for the compiled-tool case, where
105169 // masking the canonical path would also hide the file the gate must exec.
106- stash := "/tmp/.cliguard-jail"
170+ stash := jailStashPath
107171 if err := os .MkdirAll (stash , 0o700 ); err != nil {
108172 return fmt .Errorf ("sandbox: mkdir stash: %w" , err )
109173 }
110174 if err := unix .Mount ("tmpfs" , stash , "tmpfs" , 0 , "mode=0700" ); err != nil {
111175 return fmt .Errorf ("sandbox: mount stash tmpfs: %w" , err )
112176 }
113177
114- shim := spec ().SelfExe
115- for _ , t := range tools {
116- if err := installToolShim (t , stash , shim ); err != nil {
178+ mountedDirs := map [string ]struct {}{}
179+ for _ , tool := range resolvedTools {
180+ if err := stashRealBinary (tool .realPath , filepath .Join (stash , tool .tool )); err != nil {
181+ return err
182+ }
183+ if ! tool .needsLink {
184+ continue
185+ }
186+ if _ , seen := mountedDirs [tool .dir ]; seen {
187+ continue
188+ }
189+ if err := mountPrivateToolDir (tool .dir ); err != nil {
190+ return err
191+ }
192+ mountedDirs [tool .dir ] = struct {}{}
193+ }
194+ return nil
195+ }
196+
197+ // installToolShims binds the consumer shim into each wrapped tool's canonical
198+ // location after the private staging area is ready.
199+ func installToolShims (resolvedTools []resolvedTool , shim string ) error {
200+ for _ , tool := range resolvedTools {
201+ if err := installToolShim (tool , jailStashPath , shim ); err != nil {
117202 return err
118203 }
119204 }
205+ return nil
206+ }
120207
208+ func setJailEnvironment () error {
121209 if err := os .Setenv (EnvJailed , "1" ); err != nil {
122210 return fmt .Errorf ("sandbox: set jailed env: %w" , err )
123211 }
212+ return nil
213+ }
124214
215+ func clearAmbientCaps () error {
125216 // All bind-mounts are done; the tool itself must not keep CAP_SYS_ADMIN.
126217 // Clear the ambient set so the cap does not leak into the exec'd tool.
127218 if err := unix .Prctl (unix .PR_CAP_AMBIENT , unix .PR_CAP_AMBIENT_CLEAR_ALL , 0 , 0 , 0 ); err != nil {
128219 return fmt .Errorf ("sandbox: clear ambient caps: %w" , err )
129220 }
221+ return nil
222+ }
130223
131- // Drop the ability to gain privileges, then install the syscall denylist.
132- // Order matters: all bind-mounts are done above, so mount() can be denied.
133- if err := lockdownSyscalls (); err != nil {
134- return err
135- }
136-
224+ func execJailTarget (execTool string , rest []string ) error {
137225 target := os .Getenv (RealBinEnv (execTool ))
138226 if target == "" {
139227 return fmt .Errorf ("sandbox: exec target %q was not stashed (not on PATH?)" , execTool )
@@ -145,46 +233,51 @@ func RunJail(args []string) error {
145233
146234// installToolShim records one wrapped tool's real target for the gate to exec,
147235// then masks its canonical PATH entry with the consumer shim.
148- func installToolShim (tool , stash , shim string ) error {
149- canonical , err := exec .LookPath (tool )
150- if err != nil {
151- // Tool not present on this host: nothing to shim.
152- return nil //nolint:nilerr // absence is not an error; just skip
153- }
154- realPath := canonical
155- if resolved , err := filepath .EvalSymlinks (canonical ); err == nil {
156- realPath = resolved
157- }
158-
159- realTarget := realPath
160- if realPath == canonical {
161- // Compiled binary at its own path: stash a bind before masking. argv0/$0
162- // are irrelevant for an ELF, so exec'ing the stash copy is fine.
163- stashed := filepath .Join (stash , tool )
164- if err := touch (stashed ); err != nil {
165- return fmt .Errorf ("sandbox: stash placeholder %s: %w" , tool , err )
166- }
167- if err := unix .Mount (realPath , stashed , "" , unix .MS_BIND , "" ); err != nil {
168- return fmt .Errorf ("sandbox: stash bind %s: %w" , tool , err )
169- }
170- realTarget = stashed
171- } else {
172- // Symlinked $0-sensitive tool (brew derives HOMEBREW_PREFIX from $0's
173- // grandparent): exec a canonical-dir symlink so the prefix resolves right.
174- link := filepath .Join (filepath .Dir (canonical ), "." + tool + ".cliguard" )
236+ func installToolShim (tool resolvedTool , stash , shim string ) error {
237+ stashed := filepath .Join (stash , tool .tool )
238+ realTarget := stashed
239+ if tool .needsLink {
240+ // Symlinked $0-sensitive tools need argv[0] to stay beside the canonical
241+ // entry, but the exec symlink itself must stay off the host filesystem.
242+ link := filepath .Join (tool .dir , "." + tool .tool + ".cliguard" )
175243 _ = os .Remove (link )
176- if err := os .Symlink (realPath , link ); err = = nil {
177- realTarget = link
244+ if err := os .Symlink (stashed , link ); err ! = nil {
245+ return fmt . Errorf ( "sandbox: symlink exec target %s: %w" , tool . tool , err )
178246 }
247+ realTarget = link
179248 }
180- if err := os .Setenv (RealBinEnv (tool ), realTarget ); err != nil {
181- return fmt .Errorf ("sandbox: set realbin env %s: %w" , tool , err )
249+ if err := os .Setenv (RealBinEnv (tool . tool ), realTarget ); err != nil {
250+ return fmt .Errorf ("sandbox: set realbin env %s: %w" , tool . tool , err )
182251 }
183252
253+ if err := touch (tool .canonical ); err != nil {
254+ return fmt .Errorf ("sandbox: ensure canonical entry %s: %w" , tool .tool , err )
255+ }
184256 // Mask the canonical PATH/symlink entry with the shim (O_NOFOLLOW so the
185257 // bind lands on the symlink itself, leaving realTarget reachable).
186- if err := maskWithShim (canonical , shim ); err != nil {
187- return fmt .Errorf ("sandbox: shim mask %s: %w" , tool , err )
258+ if err := maskWithShim (tool .canonical , shim ); err != nil {
259+ return fmt .Errorf ("sandbox: shim mask %s: %w" , tool .tool , err )
260+ }
261+ return nil
262+ }
263+
264+ // stashRealBinary copies the resolved tool into the private tmpfs stash so the
265+ // jail can exec it after the canonical path is masked.
266+ func stashRealBinary (realPath , stashed string ) error {
267+ if err := touch (stashed ); err != nil {
268+ return fmt .Errorf ("sandbox: stash placeholder %s: %w" , filepath .Base (stashed ), err )
269+ }
270+ if err := unix .Mount (realPath , stashed , "" , unix .MS_BIND , "" ); err != nil {
271+ return fmt .Errorf ("sandbox: stash bind %s: %w" , filepath .Base (stashed ), err )
272+ }
273+ return nil
274+ }
275+
276+ // mountPrivateToolDir overlays a canonical tool directory with a private tmpfs
277+ // so sandbox-only exec symlinks never land on the host filesystem.
278+ func mountPrivateToolDir (dir string ) error {
279+ if err := unix .Mount ("tmpfs" , dir , "tmpfs" , 0 , "mode=0700" ); err != nil {
280+ return fmt .Errorf ("sandbox: mount tool dir tmpfs %s: %w" , dir , err )
188281 }
189282 return nil
190283}
0 commit comments