Skip to content

LocalREPL._temp_cwd: process-wide os.chdir races across concurrent RLM instances (FileNotFoundError, cwd cross-contamination) #180

Description

@eumemic

Bug

LocalREPL._temp_cwd() (rlm/environments/local_repl.py:505) changes the process-wide working directory around every code execution:

def _temp_cwd(self):
    old_cwd = os.getcwd()
    try:
        os.chdir(self.temp_dir)
        yield
    finally:
        os.chdir(old_cwd)

os.getcwd()/os.chdir() are global to the process, so running multiple RLM instances concurrently in threads (e.g. evaluating N samples in a ThreadPoolExecutor, each with its own RLM(environment="local")) races:

  • Thread A captures old_cwd while the process is sitting in thread B's temp_dir; B finishes and deletes its temp dir; A's finally (or its next _temp_cwd() entry) chdirs into a deleted directory:
FileNotFoundError: [Errno 2] No such file or directory: '/var/folders/.../repl_env_<uuid>_...'
  File "rlm/environments/local_repl.py", line 554, in execute_code
    with self._capture_output() as (stdout_buf, stderr_buf), self._temp_cwd():
  • Even without the crash, user code that touches relative paths can silently run in another instance's cwd. (_capture_output() on the same line redirects process-wide stdout/stderr and has the same cross-instance contamination concern.)

Repro sketch

from concurrent.futures import ThreadPoolExecutor
from rlm import RLM

def run(i):
    r = RLM(backend="openai", environment="local", max_iterations=4)
    return r.completion(f"task {i}", root_prompt="count the lines")

with ThreadPoolExecutor(max_workers=4) as pool:
    list(pool.map(run, range(16)))   # intermittent FileNotFoundError at local_repl.py:554

At 4 threads we hit the FileNotFoundError on roughly half the rollouts in some workloads (rlms==0.1.3, repo @ caf0bff, macOS).

Note this is distinct from PR #136, which parallelizes sub-calls within one RLM instance — this race is across instances sharing a process.

Suggested directions

  • Avoid chdir entirely: resolve file operations against self.temp_dir (or run user code with cwd= via a subprocess-backed executor), or
  • guard execute_code with a module-level lock (correct but serializes all local REPLs), and/or
  • document that LocalREPL is single-instance-per-process and recommend ipython subprocess mode / docker for concurrent use.

Happy to provide more detail — found while reproducing the "LM harnesses are compositional generalizers" results; we worked around it by serializing all RLM rollouts.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions