SWE-Race › Tasks › giampaolo-psutil-2515 ← prevnext →

giampaolo-psutil-2515

giampaolo/psutilcleansinglemerged 2025-03-04BSD-3-Clausefix: 2 files, +33 −111 fail-to-pass · 80 pass-to-pass
Results
Modelsolved / attemptsmedian stepsmedian costattempts
GPT-5.6 Luna6/610$0.0081✓ 2✓ 3✓ 4✓ 5✓ 6✓
DeepSeek V4 Flash2/260$0.0191✓ 2✓
GLM-5.3 Flash2/28$0.0011✓ 2✓
The prompt the agent sees

On Linux, calling `psutil.Process().cwd()` can raise `FileNotFoundError` when the process’s current-working-directory link disappears or cannot be read due to a race condition. This should be handled gracefully: the call should return an empty string instead of propagating the exception.

Hidden tests · 1 fail-to-pass, 80 pass-to-passrun after the agent submits, in a clean verifier
test_cwd_mocked
Test patch · 20 lines
diff --git a/psutil/tests/test_linux.py b/psutil/tests/test_linux.py
index 7d25fc841c..836311e220 100755
--- a/psutil/tests/test_linux.py
+++ b/psutil/tests/test_linux.py
@@ -2064,6 +2064,15 @@ def test_exe_mocked(self):
                 assert m.called
                 assert ret == ""
 
+    def test_cwd_mocked(self):
+        # https://github.com/giampaolo/psutil/issues/2514
+        with mock.patch(
+            'psutil._pslinux.readlink', side_effect=FileNotFoundError
+        ) as m:
+            ret = psutil.Process().cwd()
+            assert m.called
+            assert ret == ""
+
     def test_issue_1014(self):
         # Emulates a case where smaps file does not exist. In this case
         # wrap_exception decorator should not raise NoSuchProcess.
Reference fix · 2 files, +33 −11the upstream merge, used only for grading calibration

The agent could not see this: the repository holds one commit and the sandbox has no network. Leak audit.

HISTORY.rst, psutil/_pslinux.py

diff --git a/HISTORY.rst b/HISTORY.rst
index f84aab61cc..c05e4114f8 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -1,5 +1,15 @@
 *Bug tracker at https://github.com/giampaolo/psutil/issues*
 
+7.1.0
+=====
+
+XXXX-XX-XX
+
+**Bug fixes**
+
+- 2514_, [Linux]: `Process.cwd()`_ sometimes fail with `FileNotFoundError` due
+  to a race condition.
+
 7.0.0
 =====
 
diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py
index 8cc64e9a10..73e76297dd 100644
--- a/psutil/_pslinux.py
+++ b/psutil/_pslinux.py
@@ -83,6 +83,7 @@
 PAGESIZE = cext_posix.getpagesize()
 BOOT_TIME = None  # set later
 LITTLE_ENDIAN = sys.byteorder == 'little'
+UNSET = object()
 
 # "man iostat" states that sectors are equivalent with blocks and have
 # a size of 512 bytes. Despite this value can be queried at runtime
@@ -1698,6 +1699,22 @@ def _raise_if_not_alive(self):
         # incorrect or incomplete result.
         os.stat(f"{self._procfs_path}/{self.pid}")
 
+    def _readlink(self, path, fallback=UNSET):
+        # * https://github.com/giampaolo/psutil/issues/503
+        #   os.readlink('/proc/pid/exe') may raise ESRCH (ProcessLookupError)
+        #   instead of ENOENT (FileNotFoundError) when it races.
+        # * ENOENT may occur also if the path actually exists if PID is
+        #   a low PID (~0-20 range).
+        # * https://github.com/giampaolo/psutil/issues/2514
+        try:
+            return readlink(path)
+        except (FileNotFoundError, ProcessLookupError):
+            if os.path.lexists(f"{self._procfs_path}/{self.pid}"):
+                self._raise_if_zombie()
+                if fallback is not UNSET:
+                    return fallback
+            raise
+
     @wrap_exceptions
     @memoize_when_activated
     def _parse_stat_file(self):
@@ -1770,16 +1787,9 @@ def name(self):
 
     @wrap_exceptions
     def exe(self):
-        try:
-            return readlink(f"{self._procfs_path}/{self.pid}/exe")
-        except (FileNotFoundError, ProcessLookupError):
-            self._raise_if_zombie()
-            # no such file error; might be raised also if the
-            # path actually exists for system processes with
-            # low pids (about 0-20)
-            if os.path.lexists(f"{self._procfs_path}/{self.pid}"):
-                return ""
-            raise
+        return self._readlink(
+            f"{self._procfs_path}/{self.pid}/exe", fallback=""
+        )
 
     @wrap_exceptions
     def cmdline(self):
@@ -2054,7 +2064,9 @@ def get_blocks(lines, current_block):
 
     @wrap_exceptions
     def cwd(self):
-        return readlink(f"{self._procfs_path}/{self.pid}/cwd")
+        return self._readlink(
+            f"{self._procfs_path}/{self.pid}/cwd", fallback=""
+        )
 
     @wrap_exceptions
     def num_ctx_switches(

What each model did

Submitted diff first, then every command. Outputs show 12 lines; expand for the rest.
GLM-5.3 Flash · attempt 2 · solved · 7 steps · $0.001
Submitted diff · 19 lines
diff --git a/psutil/_pslinux.py b/psutil/_pslinux.py
index 8cc64e9..91b4cee 100644
--- a/psutil/_pslinux.py
+++ b/psutil/_pslinux.py
@@ -2054,7 +2054,13 @@ class Process:
 
     @wrap_exceptions
     def cwd(self):
-        return readlink(f"{self._procfs_path}/{self.pid}/cwd")
+        try:
+            return readlink(f"{self._procfs_path}/{self.pid}/cwd")
+        except FileNotFoundError:
+            # No such file or directory error means that either the
+            # process went away or the cwd was deleted (race condition).
+            # In both cases return an empty string instead of crashing.
+            return ""
 
     @wrap_exceptions
     def num_ctx_switches(
step 1
$ grep -rn "cwd" /repo/psutil/_pslinux.py | head -30; ls /repo
output · 18 lines
2056:    def cwd(self):
2057:        return readlink(f"{self._procfs_path}/{self.pid}/cwd")
CONTRIBUTING.md
CREDITS
HISTORY.rst
INSTALL.rst
LICENSE
MANIFEST.in
Makefile
README.rst
SECURITY.md
docs
6 more lines
make.bat
psutil
psutil.egg-info
pyproject.toml
scripts
setup.py
step 2
$ sed -n 2040,2070p /repo/psutil/_pslinux.py; grep -n "def readlink" /repo/psutil/_pslinux.py
output · 32 lines
path,
                    data.get(b'Rss:', 0),
                    data.get(b'Size:', 0),
                    data.get(b'Pss:', 0),
                    data.get(b'Shared_Clean:', 0),
                    data.get(b'Shared_Dirty:', 0),
                    data.get(b'Private_Clean:', 0),
                    data.get(b'Private_Dirty:', 0),
                    data.get(b'Referenced:', 0),
                    data.get(b'Anonymous:', 0),
                    data.get(b'Swap:', 0),
                )
20 more lines
                ls.append(item)
            return ls

    @wrap_exceptions
    def cwd(self):
        return readlink(f"{self._procfs_path}/{self.pid}/cwd")

    @wrap_exceptions
    def num_ctx_switches(
        self, _ctxsw_re=re.compile(br'ctxt_switches:\t(\d+)')
    ):
        data = self._read_status_file()
        ctxsw = _ctxsw_re.findall(data)
        if not ctxsw:
            msg = (
                "'voluntary_ctxt_switches' and"
                " 'nonvoluntary_ctxt_switches'lines were not found in"
                f" {self._procfs_path}/{self.pid}/status; the kernel is"
                " probably older than 2.6.23"
199:def readlink(path):
step 3
$ sed -n 190,215p /repo/psutil/_pslinux.py
output · 26 lines
'iowait'])
# fmt: on


# =====================================================================
# --- utils
# =====================================================================


def readlink(path):
    """Wrapper around os.readlink()."""
    assert isinstance(path, str), path
14 more lines
    path = os.readlink(path)
    # readlink() might return paths containing null bytes ('\x00')
    # resulting in "TypeError: must be encoded string without NULL
    # bytes, not str" errors when the string is passed to other
    # fs-related functions (os.*, open(), ...).
    # Apparently everything after '\x00' is garbage (we can have
    # ' (deleted)', 'new' and possibly others), see:
    # https://github.com/giampaolo/psutil/issues/717
    path = path.split('\x00')[0]
    # Certain paths have ' (deleted)' appended. Usually this is
    # bogus as the file actually exists. Even if it doesn't we
    # don't care.
    if path.endswith(' (deleted)') and not path_exists_strict(path):
        path = path[:-10]
step 4
$ cd /repo && python - <<'PY'
output · 1 lines
'/repo'
step 5
$ cd /repo && python -m pytest psutil/tests/test_linux.py -x -q 2>&1 | tail -3
output · 3 lines
FAILED psutil/tests/test_linux.py::TestSystemVirtualMemoryAgainstFree::test_available
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed in 0.68s
step 6
$ cd /repo && python -m pytest psutil/tests/test_linux.py -q -k "cwd or Process" 2>&1 | tail -3; git stash -q; python -m pytest psutil/tests/test_linux.py::TestSystemVirtualMemoryAgainstFree -q 2>&1 | tail -2; git stash pop -q
output · 4 lines
............................                                             [100%]
28 passed, 83 deselected in 0.33s
FAILED psutil/tests/test_linux.py::TestSystemVirtualMemoryAgainstFree::test_used
5 failed in 0.81s
step 7

The memory-vs-free test failures are pre-existing (they fail on the pristine tree too). My change passes all cwd-related tests.

$ done: {"notes": "psutil/_pslinux.py: Process.cwd() now catches FileNotFoundError from readlink (race where cwd link disappears) and returns \"\" instead of raising."}
output · 1 lines
submitted.