Skip to content

systemvm: fix subprocess deadlock in router health check scripts - #13737

Open
richevanscybermyte wants to merge 1 commit into
apache:mainfrom
richevanscybermyte:healthcheck-pipe-deadlock-fix
Open

systemvm: fix subprocess deadlock in router health check scripts#13737
richevanscybermyte wants to merge 1 commit into
apache:mainfrom
richevanscybermyte:healthcheck-pipe-deadlock-fix

Conversation

@richevanscybermyte

Copy link
Copy Markdown

Description

This PR fixes #13735.

The router health check scripts run shell commands with Popen(cmd, shell=True, stdout=PIPE) and then call pout.wait() before reading the pipe. When the command output exceeds the OS pipe capacity (64 KiB on Linux), the child process blocks writing to the full pipe and the parent blocks in wait() forever. This is the deadlock warned about in the Python subprocess documentation.

In iptables_check.py the command is iptables-save | grep <destIp>. On routers with large port-forwarding rule sets the output exceeds the pipe buffer and the check hangs on every run. Because the advanced health check fires every router.health.checks.advanced.interval (10 minutes by default), a new stuck process chain (monitorServices.py advanced -> sh -> iptables_check.py) accumulates every interval, about 13 MB RSS each. On a 512 MB router, RAM and swap are exhausted after roughly 9 hours, keepalived's heartbeat track script can no longer fork within its timeout, the router enters FAULT, demotes, wipes its keepalived config and reboots. Redundant pairs fail near-simultaneously because both routers are created together, so the exhaustion clocks are synchronized. See the issue for full evidence, including a control group of routers with small rule sets running the same check cleanly for 152 days.

The fix drains the pipe with communicate() and checks returncode, instead of calling wait() with an unread pipe. The same latent pattern is fixed in memory_usage_check.py, cpu_usage_check.py and gateways_check.py; their outputs are normally tiny so they do not hang today, but they share the same hazard.

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)
  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (improves an existing feature and functionality)
  • Cleanup (Code refactoring and cleanup, that may add test cases)
  • Build/CI

Bug Severity

  • BLOCKER
  • Critical
  • Major
  • Minor
  • Trivial

How Has This Been Tested?

Reproduced on CloudStack 4.22.0.0/4.22.1.0 (KVM, redundant VPC, systemvm template 4.22.0) with a VPC whose iptables-save output is 77,029 bytes. Before the fix, every advanced health check run left a permanently stuck iptables_check.py chain (verified via ps, both processes in do_wait) and router memory climbed about 87 MB/h until the router self-rebooted at about 9 hours of uptime.

With the fixed script logic (equivalent change validated on the affected router), the check completes in under a second against the same 77 KB rule set, no processes accumulate, and the router has run past the previous failure window with flat memory usage. Routers with small rule sets (1-9 KB) behave identically before and after, since their output never filled the pipe.

The health check scripts run shell commands with Popen(stdout=PIPE) and
call wait() before reading the pipe. When the command output exceeds the
OS pipe capacity (64 KiB on Linux), the child blocks writing to the full
pipe and the parent blocks in wait() forever. This is the deadlock warned
about in the Python subprocess documentation.

On routers with large iptables rule sets this makes iptables_check.py
hang on every run of the advanced health check. A new stuck process chain
accumulates each advanced-check interval (10 minutes by default) until
the router exhausts memory and swap, keepalived's track script starves,
and the router enters FAULT and reboots. On a 512 MB router this produced
a reboot roughly every 9 hours, and redundant pairs failed together
because their timers are synchronized at creation.

Drain the pipe with communicate() and check returncode instead of calling
wait() with an unread pipe. The same latent pattern is also fixed in
memory_usage_check.py, cpu_usage_check.py and gateways_check.py.

Fixes: apache#13735
@weizhouapache

Copy link
Copy Markdown
Member

@blueorangutan package

@blueorangutan

Copy link
Copy Markdown

@weizhouapache a [SL] Jenkins job has been kicked to build packages. It will be bundled with no SystemVM templates. I'll keep you posted as I make progress.

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 19.64%. Comparing base (4f11707) to head (22e9104).

Additional details and impacted files
@@             Coverage Diff              @@
##               main   #13737      +/-   ##
============================================
- Coverage     19.65%   19.64%   -0.01%     
+ Complexity    19792    19787       -5     
============================================
  Files          6368     6368              
  Lines        574881   574881              
  Branches      70351    70351              
============================================
- Hits         112970   112958      -12     
- Misses       449639   449650      +11     
- Partials      12272    12273       +1     
Flag Coverage Δ
uitests 3.41% <ø> (ø)
unittests 20.92% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 18699

@DaanHoogland

Copy link
Copy Markdown
Contributor

@blueorangutan test

@blueorangutan

Copy link
Copy Markdown

@DaanHoogland a [SL] Trillian-Jenkins test job (ol8 mgmt + kvm-ol8) has been kicked to run smoke tests

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

Labels

None yet

Projects

None yet

4 participants