Skip to content

ExerciseDir loops forever when the submit path is outside the workspace but shares its path as a string prefix #1247

Description

@krishna3554

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:

  1. 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.
  2. 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:

  1. 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 ...
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions