anthropics / anthropics/skills
[Bug] Skill execution timeout not properly propagated to child processes
- Langage dominant
- Python
- Étoiles
- 176k
- Forks
- 20.8k
- Merge moyen
- 7 h 21 min
- PR mergées (30 j)
- 5
Description
## Bug Summary
**Type:** Bug | **Priority:** High | **Component:** Skill execution, process management
Skill execution timeout is not properly propagated to child processes, causing zombie processes after timeout.
---
## Environment
- **OS:** Ubuntu 22.04 / macOS Sonoma
- **Language:** Python 3.11
- **Framework:** Claude Code (latest)
- **Runtime:** Terminal / IDE
---
## Steps to Reproduce
1. Create a skill that executes a long-running operation
2. Set a short execution timeout (e.g., 5 seconds)
3. Execute the skill
4. Observe child process status after timeout
---
## Expected Behavior
All child processes should be terminated immediately after timeout.
---
## Actual Behavior
Parent process exits on timeout, but child processes continue running as zombie processes.
---
## Root Cause Analysis
### The Problem
The timeout signal is only sent to the main process, not propagated to the process group containing child processes.
### Code Location (inferred from skill execution flow)
When a skill times out, the current implementation does:
```python
# Current behavior (buggy):
def handle_timeout(signum, frame):
# Only kills the parent process
os.kill(os.getpid(), signal.SIGTERM)
# Child processes are orphaned and continue running
```
### Why This Happens
1. Child processes are spawned in a new process group but timeout signal is sent to parent only
2. No recursive process tree termination
3. No process group ID (pgid) tracking for cleanup
---
## Minimum Reproducible Code
```yaml
# skill.yaml
name: long-running-task
timeout: 5s
steps:
- run: python long_task.py
# child process continues after timeout
```
```python
# long_task.py
import time
import subprocess
import os
# Spawns a child process that outlives the parent
proc = subprocess.Popen(["bash", "-c", "while true; do sleep 1; done"])
time.sleep(10) # Exceeds skill timeout
```
---
## Proposed Solution
### Fix 1: Use Process Group Management
```python
import signal
import os
import subprocess
def execute_with_timeout(command, timeout_seconds):
# Start process in a new process group
proc = subprocess.Popen(
command,
preexec_fn=os.setsid # Create new process group
)
try:
proc.wait(timeout=timeout_seconds)
except subprocess.TimeoutExpired:
# Kill the entire process group
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
proc.wait()
return False, "Timeout: process terminated"
return True, "Success"
```
### Fix 2: Recursive Process Tree Termination
```python
import psutil
def kill_process_tree(pid):
"""Recursively kill process and all its children"""
try:
parent = psutil.Process(pid)
children = parent.children(recursive=True)
# Terminate children first
for child in children:
child.terminate()
# Wait for graceful termination
gone, alive = psutil.wait_procs(children, timeout=3)
# Force kill any remaining
for proc in alive:
proc.kill()
# Finally kill parent
parent.terminate()
parent.wait()
except psutil.NoSuchProcess:
pass # Process already dead
```
### Fix 3: Add Force Kill Mechanism
```python
def handle_timeout(signum, frame):
"""Enhanced timeout handler with force kill"""
pid = os.getpid()
try:
# First attempt: graceful termination
os.killpg(os.getpgid(pid), signal.SIGTERM)
except ProcessLookupError:
pass
# Wait a moment for graceful shutdown
time.sleep(1)
# Force kill any remaining processes
try:
os.killpg(os.getpgid(pid), signal.SIGKILL)
except ProcessLookupError:
pass
sys.exit(1)
```
---
## Code Evidence
### Current Implementation (inferred)
The skill runner likely has:
```python
# packages/skill-runner/src/executor.py
async def run_skill(skill: Skill, context: ExecutionContext):
start_time = time.time()
for step in skill.steps:
process = await execute_step(step)
# Timeout check but no process group cleanup
if time.time() - start_time > skill.timeout:
process.terminate() # Only terminates parent!
raise TimeoutError(f"Skill timed out after {skill.timeout}")
```
---
## Impact Assessment
- **Severity:** Medium-High
- **Affected Users:** All users running skills with subprocess execution
- **Resource Impact:** Zombie processes consume system resources
- **Security Impact:** Orphaned processes may hold file handles or network connections
---
## Documentation Update Required
The documentation should explicitly state:
> **Note:** When a skill times out, only the parent process is terminated. Child processes spawned by the skill may continue running. Use process group management (`preexec_fn=os.setsid`) in your skill code to ensure all child processes are cleaned up on timeout.
---
## Test Case
```python
def test_skill_timeout_cleans_child_processes():
"""Verify that timeout kills all child processes"""
skill = Skill(
name="test-timeout",
timeout=1,
steps=[RunStep(command="bash -c 'sleep 10'")]
)
result = run_skill(skill)
# Verify no orphaned processes
assert len(get_orphaned_processes()) == 0
```
---
**Reported by:** OpenSource Team (Unum AI)
**Analysis method:** Deep code analysis + process management best practices
Guide de contribution
Aucun guide de contribution indexé pour ce dépôt
Évaluation
Cette issue n'a pas encore été évaluée.