From c36d0564814ba4c2c92ab1d470596d36f090b3c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Fri, 25 Sep 2026 17:01:07 +0200 Subject: [PATCH] Minor patch for shellExec (adding timeout) --- lib/core/common.py | 43 ++++++++++++++++++++++++++++++++++++++++--- lib/core/settings.py | 2 +- lib/core/testing.py | 4 +++- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/lib/core/common.py b/lib/core/common.py index ad437291a24..58c93fd37b6 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -27,6 +27,7 @@ import posixpath import random import re +import signal import socket import string import subprocess @@ -2476,22 +2477,58 @@ def getConsoleWidth(default=80): return width or default -def shellExec(cmd): +def shellExec(cmd, timeout=None): """ - Executes arbitrary shell command + Executes arbitrary shell command, optionally bounded by 'timeout' seconds - killing (and + flagging) a hung child instead of blocking forever, as callers otherwise have no other + watchdog around this call (e.g. --vuln-test runs one such call per entry, unattended) >>> shellExec('echo 1').strip() == '1' True """ retVal = "" + timedOut = [] try: - retVal = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()[0] or "" + popenKwargs = {"shell": True, "stdout": subprocess.PIPE, "stderr": subprocess.STDOUT} + if timeout: + # shell=True's Popen.pid is the shell, not the (possibly grandchild) command it runs - + # killing just that pid leaves the real child holding the stdout pipe open, so + # communicate() keeps blocking past the deadline; run it in its own group/session instead + # so the whole tree can be killed at once + if IS_WIN: + popenKwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP + else: + popenKwargs["preexec_fn"] = os.setsid + + process = subprocess.Popen(cmd, **popenKwargs) + + def _kill(): + timedOut.append(True) + try: + if IS_WIN: + subprocess.call(["taskkill", "/F", "/T", "/PID", str(process.pid)]) + else: + os.killpg(os.getpgid(process.pid), signal.SIGKILL) + except Exception: + pass + + timer = threading.Timer(timeout, _kill) if timeout else None + if timer: + timer.daemon = True + timer.start() + + retVal = process.communicate()[0] or "" + + if timer: + timer.cancel() except Exception as ex: retVal = getSafeExString(ex) finally: retVal = getText(retVal) + if timedOut: + retVal += "\n[shellExec] child process tree killed after exceeding %d-second timeout" % timeout return retVal diff --git a/lib/core/settings.py b/lib/core/settings.py index ca2e9f3dcec..8699ac54854 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.9.24" +VERSION = "1.10.9.25" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/lib/core/testing.py b/lib/core/testing.py index a335c09086d..2963ef91aa8 100644 --- a/lib/core/testing.py +++ b/lib/core/testing.py @@ -268,7 +268,9 @@ def _thread(): os.environ["SQLMAP_UNSAFE_EVAL"] = '1' - output = shellExec(cmd) + # bounded well above the slowest known entry (GraphQL, ~96s) - a hung entry fails fast and + # visibly instead of silently burning the whole CI job's timeout (see #6129 CI investigation) + output = shellExec(cmd, timeout=180) 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: dataToStdout("---\n\n$ %s\n" % cmd)