BUG-HUNT: [security] GitWorktreeSandbox.create() TOCTOU race — os.rmdir() then git worktree add leaves a window for symlink injection into sensitive directories #6631

Open
opened 2026-04-09 22:36:10 +00:00 by HAL9000 · 0 comments
Owner

Bug Report: Security — TOCTOU race between os.rmdir() and git worktree add

Severity Assessment

  • Impact: A local attacker can redirect the git worktree into an arbitrary directory they control or a sensitive system directory by placing a symlink at the vacated temp path during the race window, causing git to populate that directory with sandbox files
  • Likelihood: Medium — requires local access and precise timing, but the race window is non-trivial (a git command) and exploitable with inotifywait/polling
  • Priority: High

Location

  • File: src/cleveragents/infrastructure/sandbox/git_worktree.py
  • Function: GitWorktreeSandbox.create
  • Lines: ~176–194

Description

create() uses tempfile.mkdtemp() to obtain a unique directory, immediately removes it with os.rmdir(), then passes the now-vacated path to git worktree add. There is a race window between the rmdir and the git command during which an attacker can create a symlink at the exact path, redirecting the worktree into an arbitrary target directory.

Evidence

# git_worktree.py  ~L176
# Create a temporary directory for the worktree
self._worktree_path = tempfile.mkdtemp(prefix=f"ca-sandbox-{safe_plan_id}-")
# mkdtemp creates the dir; git worktree add needs it to not exist
os.rmdir(self._worktree_path)     # ← directory is deleted HERE

# ──── RACE WINDOW BEGINS ────────────────────────────────────────
# An attacker watches /tmp/ (via inotifywait) for rmdir events
# and immediately creates:
#   os.symlink("/sensitive/target", self._worktree_path)

# Create the worktree with a new branch
_run_git(
    ["worktree", "add", "-b", self._branch_name, self._worktree_path, "HEAD"],
    cwd=self._original_path,
    timeout=self._git_timeout,
)
# ──── RACE WINDOW ENDS ──────────────────────────────────────────
# git resolves the symlink and populates /sensitive/target/
# with the git worktree content (HEAD files + .git)

Attack scenario on a shared system:

  1. Unprivileged attacker monitors /tmp with inotifywait -e delete_self.
  2. cleveragents process calls tempfile.mkdtemp() → creates /tmp/ca-sandbox-plan-01ARZ-XYZ/.
  3. Process calls os.rmdir("/tmp/ca-sandbox-plan-01ARZ-XYZ/") → directory gone.
  4. Attacker atomically creates os.symlink("/important/dir", "/tmp/ca-sandbox-plan-01ARZ-XYZ").
  5. git worktree add ... /tmp/ca-sandbox-plan-01ARZ-XYZ HEAD follows the symlink and populates /important/dir with repository content.

Even if the attacker cannot time the race perfectly, the window spans the entire duration of git worktree add which can take hundreds of milliseconds on large repositories.

DoS variant: Attacker simply creates a non-empty directory at the path → git worktree add fails → SandboxCreationError on every sandbox creation attempt.

Expected Behavior

The worktree must be created in a directory that was never deleted, preventing any race window. The correct approach is to let git worktree add create the directory itself by passing a subdirectory of a parent temp dir that was never removed.

Actual Behavior

A universally writable temp directory is used. The path is reserved, then freed, then re-used — classic TOCTOU pattern.

Suggested Fix

# Create a parent temp dir (stays alive — no rmdir)
parent_tmp = tempfile.mkdtemp(prefix=f"ca-sandbox-parent-{safe_plan_id}-")
# Pass the not-yet-existing subdirectory as the worktree target
self._worktree_path = os.path.join(parent_tmp, "worktree")
# Do NOT rmdir parent_tmp; git worktree add creates the subdirectory itself.
# The parent dir is the "reservation" that prevents the race.

_run_git(
    ["worktree", "add", "-b", self._branch_name, self._worktree_path, "HEAD"],
    cwd=self._original_path,
    timeout=self._git_timeout,
)
# cleanup() must also remove parent_tmp, not just self._worktree_path

Category

security / concurrency

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: Security — TOCTOU race between `os.rmdir()` and `git worktree add` ### Severity Assessment - **Impact**: A local attacker can redirect the git worktree into an arbitrary directory they control or a sensitive system directory by placing a symlink at the vacated temp path during the race window, causing git to populate that directory with sandbox files - **Likelihood**: Medium — requires local access and precise timing, but the race window is non-trivial (a git command) and exploitable with `inotifywait`/polling - **Priority**: High ### Location - **File**: `src/cleveragents/infrastructure/sandbox/git_worktree.py` - **Function**: `GitWorktreeSandbox.create` - **Lines**: ~176–194 ### Description `create()` uses `tempfile.mkdtemp()` to obtain a unique directory, immediately removes it with `os.rmdir()`, then passes the now-vacated path to `git worktree add`. There is a race window between the `rmdir` and the git command during which an attacker can create a symlink at the exact path, redirecting the worktree into an arbitrary target directory. ### Evidence ```python # git_worktree.py ~L176 # Create a temporary directory for the worktree self._worktree_path = tempfile.mkdtemp(prefix=f"ca-sandbox-{safe_plan_id}-") # mkdtemp creates the dir; git worktree add needs it to not exist os.rmdir(self._worktree_path) # ← directory is deleted HERE # ──── RACE WINDOW BEGINS ──────────────────────────────────────── # An attacker watches /tmp/ (via inotifywait) for rmdir events # and immediately creates: # os.symlink("/sensitive/target", self._worktree_path) # Create the worktree with a new branch _run_git( ["worktree", "add", "-b", self._branch_name, self._worktree_path, "HEAD"], cwd=self._original_path, timeout=self._git_timeout, ) # ──── RACE WINDOW ENDS ────────────────────────────────────────── # git resolves the symlink and populates /sensitive/target/ # with the git worktree content (HEAD files + .git) ``` **Attack scenario on a shared system:** 1. Unprivileged attacker monitors `/tmp` with `inotifywait -e delete_self`. 2. `cleveragents` process calls `tempfile.mkdtemp()` → creates `/tmp/ca-sandbox-plan-01ARZ-XYZ/`. 3. Process calls `os.rmdir("/tmp/ca-sandbox-plan-01ARZ-XYZ/")` → directory gone. 4. Attacker atomically creates `os.symlink("/important/dir", "/tmp/ca-sandbox-plan-01ARZ-XYZ")`. 5. `git worktree add ... /tmp/ca-sandbox-plan-01ARZ-XYZ HEAD` follows the symlink and populates `/important/dir` with repository content. Even if the attacker cannot time the race perfectly, the window spans the entire duration of `git worktree add` which can take hundreds of milliseconds on large repositories. **DoS variant**: Attacker simply creates a non-empty directory at the path → `git worktree add` fails → `SandboxCreationError` on every sandbox creation attempt. ### Expected Behavior The worktree must be created in a directory that was never deleted, preventing any race window. The correct approach is to let `git worktree add` create the directory itself by passing a subdirectory of a parent temp dir that was never removed. ### Actual Behavior A universally writable temp directory is used. The path is reserved, then freed, then re-used — classic TOCTOU pattern. ### Suggested Fix ```python # Create a parent temp dir (stays alive — no rmdir) parent_tmp = tempfile.mkdtemp(prefix=f"ca-sandbox-parent-{safe_plan_id}-") # Pass the not-yet-existing subdirectory as the worktree target self._worktree_path = os.path.join(parent_tmp, "worktree") # Do NOT rmdir parent_tmp; git worktree add creates the subdirectory itself. # The parent dir is the "reservation" that prevents the race. _run_git( ["worktree", "add", "-b", self._branch_name, self._worktree_path, "HEAD"], cwd=self._original_path, timeout=self._git_timeout, ) # cleanup() must also remove parent_tmp, not just self._worktree_path ``` ### Category security / concurrency ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: `@tdd_issue`, `@tdd_issue_<this-issue-number>`, and `@tdd_expected_fail` to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-09 22:47:12 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#6631
No description provided.