Skip to content

fix(system): Handle psutil.AccessDenied in get_memory_info#2078

Open
anxkhn wants to merge 1 commit into
apify:masterfrom
anxkhn:fix/memory-info-access-denied
Open

fix(system): Handle psutil.AccessDenied in get_memory_info#2078
anxkhn wants to merge 1 commit into
apify:masterfrom
anxkhn:fix/memory-info-access-denied

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

get_memory_info() in src/crawlee/_utils/system.py can crash with psutil.AccessDenied in restricted environments, which silently stops the autoscaler's CPU/memory snapshots.

  • On Linux, _get_used_memory reads a process's PSS via process.memory_full_info() (/proc/<pid>/smaps). psutil documents that memory_full_info / PSS may require elevated privileges, so in a hardened container (hidepid, restricted /proc, seccomp) or for an unreadable child subprocess it raises psutil.AccessDenied.
  • psutil.AccessDenied is not a subclass of psutil.NoSuchProcess (MRO: AccessDenied -> Error -> Exception), so the existing with suppress(psutil.NoSuchProcess) around the child loop did not catch it, and the current-process read at the top of the function was not guarded at all.
  • When it propagates, it escapes the recurring SYSTEM_INFO task (RecurringTask._wrapper has no error handling), so the recurring task stops and the autoscaler quietly stops receiving snapshots.

Fix, kept minimal:

-    # Retrieve estimated memory usage of the current process.
-    current_size_bytes = _get_used_memory(current_process)
+    # Retrieve estimated memory usage of the current process. On Linux `_get_used_memory` reads PSS via
+    # `memory_full_info`, which can raise `AccessDenied` in restricted environments (e.g. hardened containers);
+    # fall back to RSS, which a process can always read for itself.
+    try:
+        current_size_bytes = _get_used_memory(current_process)
+    except psutil.AccessDenied:
+        current_size_bytes = int(current_process.memory_info().rss)

     # Sum memory usage by all children processes, try to exclude shared memory from the sum if allowed by OS.
     for child in current_process.children(recursive=True):
-        # Ignore any NoSuchProcess exception that might occur if a child process ends before we retrieve
-        # its memory usage.
-        with suppress(psutil.NoSuchProcess):
+        # Ignore a child that ends before we retrieve its memory usage (`NoSuchProcess`) or that we are not
+        # allowed to inspect (`AccessDenied`, e.g. an unreadable subprocess in a restricted environment).
+        with suppress(psutil.NoSuchProcess, psutil.AccessDenied):
             current_size_bytes += _get_used_memory(child)

A process can always read its own RSS, so the current-process fallback keeps a valid (slightly higher, RSS vs PSS) estimate instead of aborting the whole snapshot; an unreadable child is skipped just like a dead one.

Issues

  • No existing GitHub issue. This is a self-identified defensive fix found while reviewing get_memory_info for cross-platform robustness (in the same spirit as the earlier PSS-on-Linux and macOS memory-estimation fixes to this file). Happy to open a tracking issue with the repro first if you'd prefer.
  • Related but not a duplicate: fix: prevent silent RecurringTask death and swallowed timeouts in AutoscaledPool #2009 hardens the generic recurring-task pathway so a failing task logs and continues instead of dying silently. This PR fixes the root cause so get_memory_info does not raise AccessDenied in the first place (no file overlap; the two are complementary).

Testing

  • Added two tests to tests/unit/_utils/test_system.py:
    • test_get_memory_info_skips_children_with_access_denied: a child that raises AccessDenied is skipped rather than aborting the snapshot.
    • test_get_memory_info_falls_back_to_rss_when_current_process_access_denied: PSS denial for the current process falls back to RSS.
  • Verified fail-first: reverting only src/crawlee/_utils/system.py (keeping the new tests) makes both tests fail with psutil.AccessDenied propagating out of get_memory_info(); they pass with the fix.
  • uv run pytest tests/unit/_utils/test_system.py -> 4 passed, 1 skipped (the skip is the Linux-only shared-memory test).
  • Ran the downstream consumers too: uv run pytest tests/unit/events/ tests/unit/_autoscaling/ -> green.
  • uv run poe lint (ruff format + check) and uv run ty check on the changed files both pass.

Checklist

  • CI passed

On Linux, get_memory_info reads a process's PSS via memory_full_info,
which psutil documents may require elevated privileges. In restricted
environments (hardened containers with hidepid/restricted /proc, or an
unreadable child subprocess) this raises psutil.AccessDenied, which is
not a subclass of psutil.NoSuchProcess and so was not caught by the
existing suppress around the child loop; the current-process read was
unguarded entirely. The exception propagated out of the recurring
system-info task, which has no error handling, silently stopping the
autoscaler's CPU/memory snapshots.

Suppress AccessDenied alongside NoSuchProcess when summing child memory,
and fall back to RSS for the current process when PSS is denied.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants