Summary
Workspace.ExerciseDir can enter an infinite loop (CLI hangs at 100% CPU) when the submitted path is outside the workspace but happens to share the workspace path as a string prefix — e.g. workspace ~/exercism and submit path ~/exercism-other/solution/main.go. The prefix guard passes, and the walk-up loop then has no terminating condition because every ancestor directory exists all the way to the filesystem root.
Location
if !strings.HasPrefix(s, ws.Dir) { // line 102 – no path-boundary check
...
return "", err
}
path := s
for {
if path == ws.Dir {
return "", errMissingMetadata
}
if _, err := os.Lstat(path); os.IsNotExist(err) { // only non-ws.Dir exit is a missing ancestor
return "", err
}
...metadata checks...
path = filepath.Dir(path)
}
Problem
Two defects interact:
- Prefix boundary bug (line 102):
strings.HasPrefix(s, ws.Dir) accepts paths in sibling directories whose name extends the workspace name (~/exercism vs ~/exercism-dev), since there is no check for a following path separator. These directories are not inside the workspace.
- Loop termination depends on reaching
ws.Dir exactly or a nonexistent ancestor. For such sibling paths, walking up with filepath.Dir visits only directories that exist (…/exercism-dev → home → /). Since none equals ws.Dir, filepath.Dir("/") == "/", and os.Lstat("/") succeeds, the loop never terminates.
Reachable through exercism submit <path>: cmd/submit.go resolves the user-supplied path via filepath.Abs + filepath.EvalSymlinks and passes it to ws.ExerciseDir (exercise() at cmd/submit.go:198, second call site at line 376).
Trigger / Reproduction
Static analysis finding — not confirmed by execution; derived from the control flow on current main (2a86fe39):
workspace: /home/u/exercism (configured workspace, contains no metadata above exercises)
submit: /home/u/exercism-dev/p/practice/foo.go (sibling tree sharing the string prefix)
HasPrefix passes ("exercism" is a prefix of "exercism-dev")
- Walk-up visits
…/exercism-dev/p, …/exercism-dev, /home/u, /home, /
- No
.exercise-metadata.ui.json/legacy metadata exists at any of these levels, so path = filepath.Dir(path) cycles at "/" forever.
Note that the existing test table in workspace/workspace_test.go (TestExerciseDir) covers ws.Dir/not-exercise/... (terminates at ws.Dir) and a parent-of-workspace path (fails the prefix check), but no case where a sibling shares a string prefix — which is precisely the hanging combination.
Expected Behavior
A submit path outside the workspace should produce an error (e.g. not in workspace); the walk-up loop should terminate when it climbs above ws.Dir.
Actual Behavior
Per the code path, the CLI spins in the loop indefinitely and must be killed.
Impact
A typo'd submit path (exercism vs exercism-old, or any directory whose name starts with the workspace directory's name) hangs the CLI instead of returning an error.
Suggested Direction
Two complementary fixes:
- Use a boundary-aware containment check instead of raw
HasPrefix, e.g. compare s against ws.Dir + string(os.PathSeparator) (after cleaning both), or use filepath.Rel(ws.Dir, s) and reject results starting with ...
- As a belt-and-braces loop guard, stop once
len(path) <= len(ws.Dir) (or once filepath.Dir(path) == path) and return an error.
Happy to help test a patch.
Summary
Workspace.ExerciseDircan enter an infinite loop (CLI hangs at 100% CPU) when the submitted path is outside the workspace but happens to share the workspace path as a string prefix — e.g. workspace~/exercismand submit path~/exercism-other/solution/main.go. The prefix guard passes, and the walk-up loop then has no terminating condition because every ancestor directory exists all the way to the filesystem root.Location
workspace/workspace.goExerciseDir(lines 101–127)Problem
Two defects interact:
strings.HasPrefix(s, ws.Dir)accepts paths in sibling directories whose name extends the workspace name (~/exercismvs~/exercism-dev), since there is no check for a following path separator. These directories are not inside the workspace.ws.Direxactly or a nonexistent ancestor. For such sibling paths, walking up withfilepath.Dirvisits only directories that exist (…/exercism-dev→ home →/). Since none equalsws.Dir,filepath.Dir("/") == "/", andos.Lstat("/")succeeds, the loop never terminates.Reachable through
exercism submit <path>:cmd/submit.goresolves the user-supplied path viafilepath.Abs+filepath.EvalSymlinksand passes it tows.ExerciseDir(exercise()at cmd/submit.go:198, second call site at line 376).Trigger / Reproduction
Static analysis finding — not confirmed by execution; derived from the control flow on current
main(2a86fe39):HasPrefixpasses ("exercism" is a prefix of "exercism-dev")…/exercism-dev/p,…/exercism-dev,/home/u,/home,/.exercise-metadata.ui.json/legacy metadata exists at any of these levels, sopath = filepath.Dir(path)cycles at"/"forever.Note that the existing test table in
workspace/workspace_test.go(TestExerciseDir) coversws.Dir/not-exercise/...(terminates atws.Dir) and a parent-of-workspace path (fails the prefix check), but no case where a sibling shares a string prefix — which is precisely the hanging combination.Expected Behavior
A submit path outside the workspace should produce an error (e.g.
not in workspace); the walk-up loop should terminate when it climbs abovews.Dir.Actual Behavior
Per the code path, the CLI spins in the loop indefinitely and must be killed.
Impact
A typo'd submit path (
exercismvsexercism-old, or any directory whose name starts with the workspace directory's name) hangs the CLI instead of returning an error.Suggested Direction
Two complementary fixes:
HasPrefix, e.g. comparesagainstws.Dir + string(os.PathSeparator)(after cleaning both), or usefilepath.Rel(ws.Dir, s)and reject results starting with...len(path) <= len(ws.Dir)(or oncefilepath.Dir(path) == path) and return an error.Happy to help test a patch.