[CRITICAL] Port reservation leak when start_model fails between reserve and Popen (process_manager.py:127-142, 262-272) #40
Labels
No labels
blocked:lint-failed
blocked:needs-decision
blocked:no-changes
blocked:no-procedure
blocked:preflight
blocked:verify-failed
bug
feature-request
in-progress
lint-cleanup
ops-task
quickfix
security/high
security/medium
test-coverage
triage-approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Mark/vllm_mm#40
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
If
start_modelfails betweenreserve_process_idandPopen, the reserved port is never released —stop_modelearly-returns because the process isn't registered. After ~1000 failed starts the port pool is exhausted.Location
backend/app/services/process_manager.pylines 123-142 (reserve_process_id)backend/app/services/process_manager.pylines 262-272 (start_modelexception handler)backend/app/services/process_manager.pylines 404-435 (stop_model)Bug
reserve_process_idrecordsself._ports[process_id] = port(line 133) but does not store aPopen. Ifstart_modelraises between the reserve call and line 220 (self._processes[process_id] = process), the exception handler at line 262-272 callsawait self.stop_model(process_id).stop_modelchecksif process_id not in self._processes: return Falseat line 406-407 and bails without touching_ports.Failure windows where this leaks:
_download_gguf_modelraises (lines 188, 274-402 —RuntimeError, HTTP error, disk full, OOM during download).os.makedirs(log_dir, exist_ok=True)fails (line 226).open(log_file, "w")fails (line 231) — read-only filesystem, permission denied.subprocess.Popen(...)itself raises (line 212-218) — for example, the executable is missing.Each leak consumes one port from the
_base_port.._max_portrange (defaults 18000-19000 = 1000 ports).Reproduction
Expected: after enough failures to consume the range, a subsequent call raises
RuntimeError("No free ports available...").Suggested Fix
In
start_model's exception block (around line 262), before callingstop_model:Alternatively, teach
reserve_process_idto track areservedflag andstop_modelto honor it.Severity
Critical — silent resource leak that hard-fails the backend after enough failed starts. Especially dangerous in CI / scripted multi-model workflows.
Detected
Code review of HEAD
e91deb0(2026-09-14).Verification complete — fix verified on remote.
4d96d915be55d34b6e41f564aa90898ad196e718("Fix #40: release reserved port on failed start_model")git push origin mainsucceeded —ea16ebf..4d96d91 main -> mainTestPortReservationLeak(FileNotFoundError-from-Popen and immediate-exit paths throughreserve_process_id -> start_model)elsebranch tostart_modelexception handler inbackend/app/services/process_manager.pythat releases the reserved port when the subprocess never registered (otherwisestop_modelearly-returns on the missing process_id and_portsleaks until the 1000-entry pool is exhausted).closed(closed_at 2026-09-18T23:13:07Z) with labelsbug,triage-approved,vllm_mm.Closing the fixit board card.