Skip to content

Commit c36d056

Browse files
committed
Minor patch for shellExec (adding timeout)
1 parent a3d2917 commit c36d056

3 files changed

Lines changed: 44 additions & 5 deletions

File tree

‎lib/core/common.py‎

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import posixpath
2828
import random
2929
import re
30+
import signal
3031
import socket
3132
import string
3233
import subprocess
@@ -2476,22 +2477,58 @@ def getConsoleWidth(default=80):
24762477

24772478
return width or default
24782479

2479-
def shellExec(cmd):
2480+
def shellExec(cmd, timeout=None):
24802481
"""
2481-
Executes arbitrary shell command
2482+
Executes arbitrary shell command, optionally bounded by 'timeout' seconds - killing (and
2483+
flagging) a hung child instead of blocking forever, as callers otherwise have no other
2484+
watchdog around this call (e.g. --vuln-test runs one such call per entry, unattended)
24822485
24832486
>>> shellExec('echo 1').strip() == '1'
24842487
True
24852488
"""
24862489

24872490
retVal = ""
2491+
timedOut = []
24882492

24892493
try:
2490-
retVal = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()[0] or ""
2494+
popenKwargs = {"shell": True, "stdout": subprocess.PIPE, "stderr": subprocess.STDOUT}
2495+
if timeout:
2496+
# shell=True's Popen.pid is the shell, not the (possibly grandchild) command it runs -
2497+
# killing just that pid leaves the real child holding the stdout pipe open, so
2498+
# communicate() keeps blocking past the deadline; run it in its own group/session instead
2499+
# so the whole tree can be killed at once
2500+
if IS_WIN:
2501+
popenKwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
2502+
else:
2503+
popenKwargs["preexec_fn"] = os.setsid
2504+
2505+
process = subprocess.Popen(cmd, **popenKwargs)
2506+
2507+
def _kill():
2508+
timedOut.append(True)
2509+
try:
2510+
if IS_WIN:
2511+
subprocess.call(["taskkill", "/F", "/T", "/PID", str(process.pid)])
2512+
else:
2513+
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
2514+
except Exception:
2515+
pass
2516+
2517+
timer = threading.Timer(timeout, _kill) if timeout else None
2518+
if timer:
2519+
timer.daemon = True
2520+
timer.start()
2521+
2522+
retVal = process.communicate()[0] or ""
2523+
2524+
if timer:
2525+
timer.cancel()
24912526
except Exception as ex:
24922527
retVal = getSafeExString(ex)
24932528
finally:
24942529
retVal = getText(retVal)
2530+
if timedOut:
2531+
retVal += "\n[shellExec] child process tree killed after exceeding %d-second timeout" % timeout
24952532

24962533
return retVal
24972534

‎lib/core/settings.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.9.24"
23+
VERSION = "1.10.9.25"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)

‎lib/core/testing.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,9 @@ def _thread():
268268

269269
os.environ["SQLMAP_UNSAFE_EVAL"] = '1'
270270

271-
output = shellExec(cmd)
271+
# bounded well above the slowest known entry (GraphQL, ~96s) - a hung entry fails fast and
272+
# visibly instead of silently burning the whole CI job's timeout (see #6129 CI investigation)
273+
output = shellExec(cmd, timeout=180)
272274

273275
if not all((check in output if not check.startswith('~') else check[1:] not in output) for check in checks) or "unhandled exception" in output:
274276
dataToStdout("---\n\n$ %s\n" % cmd)

0 commit comments

Comments
 (0)