From 3474056153e09878e9216300b1a211d578a1588d Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:37:31 +0100 Subject: [PATCH 01/19] Only select verified installed Solidity compilers --- solc_util.py | 190 ++++++++++++++++----------------------------------- 1 file changed, 59 insertions(+), 131 deletions(-) diff --git a/solc_util.py b/solc_util.py index c3c6ccd..a649ca1 100644 --- a/solc_util.py +++ b/solc_util.py @@ -28,7 +28,7 @@ def _env_default_solc() -> str: """Pick a default solc that actually exists when possible.""" requested = (os.getenv("SOLC_VERSION") or "").strip() # Prefer explicit env only as a hint — still validated later - return requested or "0.8.20" + return requested or "0.8.38" DEFAULT_SOLC = _env_default_solc() @@ -159,24 +159,22 @@ def solc_select_installed_versions() -> List[str]: def installed_solc_versions() -> List[str]: - versions: List[str] = [] + """Return only compiler versions backed by an executable binary.""" + candidates: set[str] = set() for root in _solc_roots(): artifacts = root / "artifacts" - if not artifacts.is_dir(): - continue - for item in artifacts.glob("solc-*"): - version = item.name.removeprefix("solc-") - if re.fullmatch(r"\d+\.\d+\.\d+", version) and solc_artifact_path(version): - versions.append(version) - - path_ver, path_bin = path_solc_info() - if path_bin and path_ver and re.fullmatch(r"\d+\.\d+\.\d+", path_ver): - versions.append(path_ver) - - # solc-select may report versions whose binaries live outside artifact scan - versions.extend(solc_select_installed_versions()) - - return sorted(set(versions), key=Version) + if artifacts.is_dir(): + for item in artifacts.glob("solc-*"): + version = item.name.removeprefix("solc-") + if re.fullmatch(r"\\d+\\.\\d+\\.\\d+", version): + candidates.add(version) + # solc-select inventory is discovery metadata, not proof of a usable binary. + candidates.update(solc_select_installed_versions()) + path_version, path_binary = path_solc_info() + if path_binary and path_version and re.fullmatch(r"\\d+\\.\\d+\\.\\d+", path_version): + candidates.add(path_version) + verified = [version for version in candidates if solc_artifact_path(version)] + return sorted(set(verified), key=Version) def install_solc(version: str) -> Optional[Path]: @@ -216,140 +214,70 @@ def install_solc(version: str) -> Optional[Path]: def extract_solc_version(source_code: str, *, require_pragma: bool = False) -> Optional[str]: - match = re.search(r"pragma\s+solidity\s+([^;]+);", source_code) + """Choose the highest verified installed compiler compatible with the pragma.""" + match = re.search(r"pragma\\s+solidity\\s+([^;]+);", source_code) if require_pragma and not match: return None - spec = match.group(1).strip() if match else DEFAULT_SOLC - try: - constraint = NpmSpec(spec) - except ValueError: - logger.warning("Unsupported Solidity pragma constraint: %s", spec) - return None - available = installed_solc_versions() - # Merge solc-select reported versions even if binary path not resolved yet - try: - available = sorted(set(available) | set(solc_select_installed_versions()), key=Version) - except Exception: - pass - - matches = [v for v in available if constraint.match(Version(v))] - if matches: - return sorted(matches, key=Version)[-1] - - path_ver, path_bin = path_solc_info() - if path_bin and path_ver: - try: - if constraint.match(Version(path_ver)): - return path_ver - except ValueError: - pass - - # Exact version requested — try install / solc-select use - if re.fullmatch(r"\d+\.\d+\.\d+", spec): - if install_solc(spec): - return spec - # Never return a version that is not installed - if available: - try: - # closest higher matching major.minor if any - same_minor = [v for v in available if v.startswith(spec.rsplit(".", 1)[0])] - if same_minor: - return sorted(same_minor, key=Version)[-1] - return sorted(available, key=Version)[-1] - except Exception: - return available[-1] + if not available: return None - - for candidate in _PREFERRED_SOLC: + if match: + spec = match.group(1).strip() try: - if not constraint.match(Version(candidate)): - continue - if candidate in available or install_solc(candidate): - return candidate - except Exception: - continue - - # Any available version that satisfies the pragma - if matches: - return sorted(matches, key=Version)[-1] - - # Last resort: highest installed compiler (better than demanding missing 0.8.0) - if available: - logger.warning("No exact pragma match; using highest installed solc %s", available[-1]) - return available[-1] - - if path_bin and path_ver: - return path_ver - - return None + constraint = NpmSpec(spec) + compatible = [v for v in available if constraint.match(Version(v))] + if compatible: + return sorted(compatible, key=Version)[-1] + logger.warning("No installed solc satisfies pragma %s; using highest installed %s for compile diagnostics", spec, available[-1]) + except ValueError: + logger.warning("Unsupported Solidity pragma %s; using highest installed compiler", spec) + # No pragma / unsupported pragma / no compatible compiler: never return + # DEFAULT_SOLC blindly. The caller pins its temporary source to this binary. + return available[-1] def resolve_solc_binary(version: Optional[str]): - if not version: - path_ver, path_bin = path_solc_info() - if path_bin: - return path_ver, str(path_bin) - return None, None - path = solc_artifact_path(version) or install_solc(version) - if path: - return version, str(path) - - # Try solc-select use then re-probe PATH - try: - subprocess.run( - ["solc-select", "use", version], - capture_output=True, text=True, timeout=60, check=False, - ) + """Resolve only a verified compiler; never return a requested missing version.""" + if version: + path = solc_artifact_path(version) + if path: + return version, str(path) + try: + subprocess.run(["solc-select", "use", version], capture_output=True, text=True, timeout=60, check=False) + except Exception as exc: + logger.warning("solc-select use %s failed: %s", version, exc) global _PATH_SOLC_CACHE _PATH_SOLC_CACHE = None - path_ver, path_bin = path_solc_info() - if path_bin and path_ver == version: - return version, str(path_bin) - # Even if --version still mismatches, PATH solc may be the selected binary - if path_bin: - path2 = solc_artifact_path(version) - if path2: - return version, str(path2) - except Exception as exc: - logger.warning("solc-select use %s failed: %s", version, exc) - - path_ver, path_bin = path_solc_info() - if path_bin and path_ver == version: - return version, str(path_bin) - - return version, None + path_version, path_binary = path_solc_info() + if path_binary and path_version == version: + return version, str(path_binary) + return None, None + path_version, path_binary = path_solc_info() + if path_binary and path_version: + return path_version, str(path_binary) + return None, None def solc_status() -> Dict[str, Any]: - path_ver, path_bin = path_solc_info() + path_version, path_binary = path_solc_info() versions = installed_solc_versions() - default_path = solc_artifact_path(DEFAULT_SOLC) - if not default_path and path_bin: - default_path = path_bin - select_versions = solc_select_installed_versions() - compilers_ready = len(versions) > 0 or bool(path_bin) or len(select_versions) > 0 - # Prefer the highest installed 0.8.x as effective default when DEFAULT is missing - effective_default = DEFAULT_SOLC - try: - if versions: - preferred = [v for v in versions if v.startswith("0.8.")] - if preferred: - effective_default = preferred[-1] - except Exception: - pass + preferred = [v for v in versions if v.startswith("0.8.")] + effective_default = preferred[-1] if preferred else (versions[-1] if versions else None) + default_binary = solc_artifact_path(effective_default) if effective_default else None + if not default_binary and path_binary and path_version == effective_default: + default_binary = path_binary return { "default_version": DEFAULT_SOLC, "effective_default": effective_default, "versions": versions, "version_count": len(versions), "solc_select_versions": select_versions, - "default_binary_found": bool(default_path), - "default_binary_path": str(default_path) if default_path else None, - "path_solc_version": path_ver, - "path_solc_binary": str(path_bin) if path_bin else None, - "compilers_ready": compilers_ready, - "search_roots": [str(r) for r in _solc_roots()], + "default_binary_found": bool(default_binary), + "default_binary_path": str(default_binary) if default_binary else None, + "path_solc_version": path_version, + "path_solc_binary": str(path_binary) if path_binary else None, + "compilers_ready": bool(versions), + "search_roots": [str(root) for root in _solc_roots()], "solc_select_install_dir": os.getenv("SOLC_SELECT_INSTALL_DIR"), } From b999449f42f881c8af2e38b88941bbc508e56fe0 Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:37:52 +0100 Subject: [PATCH 02/19] Coerce F1 to verified solc and retry Python API once --- slither_runner.py | 150 ++++++++++++++++++++++------------------------ 1 file changed, 70 insertions(+), 80 deletions(-) diff --git a/slither_runner.py b/slither_runner.py index 18c1196..8f579de 100644 --- a/slither_runner.py +++ b/slither_runner.py @@ -32,7 +32,7 @@ SLITHER_TIMEOUT = int(os.getenv("SLITHER_TIMEOUT_SECONDS", "90")) ENABLE_SLITHER = os.getenv("ENABLE_SLITHER", "true").strip().lower() in {"1", "true", "yes", "on"} -DEFAULT_SOLC = os.getenv("SOLC_VERSION", "0.8.20").strip() or "0.8.20" +DEFAULT_SOLC = os.getenv("SOLC_VERSION", "0.8.38").strip() or "0.8.38" KEEP_IMPACTS = {"High", "Medium"} PRIORITY_CHECKS = { "reentrancy-eth", "reentrancy-no-eth", "reentrancy-benign", "reentrancy-events", @@ -363,52 +363,53 @@ def _init_slither(path: Path, kw: dict): if sl is None and init_err is not None: err_text = str(init_err) logger.warning("Slither() failed: %s", err_text) - # e.g. "solc 0.8.0 not installed, only 0.8.38 available" - m = re.search(r"only\s+([0-9]+\.[0-9]+\.[0-9]+)\s+available", err_text, re.I) - if not m: - m = re.search(r"solc\s+([0-9]+\.[0-9]+\.[0-9]+)\s+not installed", err_text, re.I) - # If we only know the missing version, try highest preferred installed - if m: - try: - from solc_util import solc_select_installed_versions, installed_solc_versions as _iv - avail = sorted(set(_iv()) | set(solc_select_installed_versions()), key=Version) - if avail: - # synthesize match for retry path below - class _M: - def group(self, _i): - return avail[-1] - m = _M() - except Exception: - m = None - if m: - alt = m.group(1) - logger.info("Retrying Slither with host solc %s", alt) - try: - subprocess.run(["solc-select", "use", alt], capture_output=True, text=True, timeout=60, check=False) - except Exception: - pass - # Rewrite pragma to exact available version so compile matches + # A host can report either the missing requested compiler or the + # only compiler it has. Retry once, but only with a verified binary. + missing_match = re.search(r"solc\\s+([0-9]+\\.[0-9]+\\.[0-9]+)\\s+not installed", err_text, re.I) + available_match = re.search(r"only\\s+([0-9]+\\.[0-9]+\\.[0-9]+)\\s+available", err_text, re.I) + try: + available = sorted(set(_util_installed_solc_versions()), key=Version) + except Exception: + available = [] + retry_version = None + if available_match and available_match.group(1) in available: + retry_version = available_match.group(1) + elif missing_match and available: + retry_version = available[-1] + if retry_version: + logger.info("Retrying Slither Python API once with installed solc %s", retry_version) try: - fixed = re.sub( - r"pragma\s+solidity\s+[^;]+;", - f"pragma solidity {alt};", - sol_file.read_text(encoding="utf-8"), - count=1, + subprocess.run( + ["solc-select", "use", retry_version], + capture_output=True, text=True, timeout=60, check=False, + ) + except Exception as exc: + logger.warning("solc-select use %s failed: %s", retry_version, exc) + resolved_version, retry_bin = _util_resolve_solc_binary(retry_version) + if retry_bin and resolved_version == retry_version: + try: + original = sol_file.read_text(encoding="utf-8") + pinned = f"pragma solidity {retry_version};" + if re.search(r"pragma\\s+solidity\\s+[^;]+;", original): + fixed = re.sub(r"pragma\\s+solidity\\s+[^;]+;", pinned, original, count=1) + else: + fixed = pinned + "\\n" + original + sol_file.write_text(fixed, encoding="utf-8") + alt_kwargs = dict(kwargs) + alt_kwargs["solc"] = str(retry_bin) + parent = str(Path(retry_bin).parent) + os.environ["PATH"] = parent + os.pathsep + os.environ.get("PATH", "") + os.environ["SOLC_BINARY"] = str(retry_bin) + os.environ["SOLC"] = str(retry_bin) + sl, init_err = _init_slither(sol_file, alt_kwargs) + except Exception as retry_exc: + init_err = retry_exc + else: + init_err = RuntimeError( + f"Compiler {retry_version} was reported but no matching executable binary could be resolved" ) - sol_file.write_text(fixed, encoding="utf-8") - except Exception: - pass - alt_bin = None - try: - from solc_util import resolve_solc_binary as _rs - _, alt_bin = _rs(alt) - except Exception: - alt_bin = solc_bin - alt_kw = dict(kwargs) - if alt_bin: - alt_kw["solc"] = str(alt_bin) - sl, init_err = _init_slither(sol_file, alt_kw) if sl is None: + error_text = str(init_err or err_text) return { "success": False, "detectors": [], @@ -417,8 +418,8 @@ def group(self, _i): "count": 0, "filtered_count": 0, "raw": None, - "error": f"Slither API init failed: {init_err}", - "error_kind": _classify_slither_error(str(init_err)), + "error": f"Slither compile failed: {error_text}", + "error_kind": _classify_slither_error(error_text) or "compile_failed", } # Register stock detectors if not already present @@ -518,41 +519,27 @@ def run_slither(source_code: str, contract_name: str = "Contract.sol", *, requir if require_pragma and not pragma_match: return {"success": False, "detectors": [], "filtered_detectors": [], "detector_checks": [], "count": 0, "filtered_count": 0, "raw": None, "error": "unverified: patch has no Solidity pragma.", "error_kind": "compile_failed"} rewritten = rewrite_known_oz_imports(source_code) - selected_version = _extract_solc_version(source_code) - # Always coerce to a compiler that is actually installed on this host + # Select only verified installed binaries. Do not trust solc-select's + # inventory unless the corresponding executable can actually be resolved. try: - from solc_util import solc_select_installed_versions, installed_solc_versions as _iv - available = sorted(set(_iv()) | set(solc_select_installed_versions()), key=Version) - if available: - if not selected_version or selected_version not in available: - pragma_m = re.search(r"pragma\s+solidity\s+([^;]+);", source_code) - if pragma_m: - try: - constraint = NpmSpec(pragma_m.group(1).strip()) - matching = [v for v in available if constraint.match(Version(v))] - selected_version = sorted(matching, key=Version)[-1] if matching else available[-1] - except ValueError: - selected_version = available[-1] - else: - # No pragma (legacy 0.4-style): use highest available; compile may still fail - selected_version = available[-1] - logger.info("Coerced solc selection to installed %s (available=%s)", selected_version, available) - else: - # Prefer highest matching available for caret/range pragmas - pragma_m = re.search(r"pragma\s+solidity\s+([^;]+);", source_code) - if pragma_m: - try: - constraint = NpmSpec(pragma_m.group(1).strip()) - matching = [v for v in available if constraint.match(Version(v))] - if matching: - best = sorted(matching, key=Version)[-1] - if best != selected_version: - logger.info("Upgrading selected solc %s → %s", selected_version, best) - selected_version = best - except ValueError: - pass + available = sorted(set(_installed_solc_versions()), key=Version) except Exception as exc: - logger.warning("solc version coercion failed: %s", exc) + logger.warning("Could not enumerate installed solc binaries: %s", exc) + available = [] + selected_version = None + if available: + if pragma_match: + try: + constraint = NpmSpec(pragma_match.group(1).strip()) + matching = [version for version in available if constraint.match(Version(version))] + selected_version = matching[-1] if matching else available[-1] + except ValueError: + selected_version = available[-1] + else: + # Legacy/no-pragma sources use the highest installed compiler; + # real syntax incompatibility is returned as a compile error. + selected_version = available[-1] + logger.info("F1 selected verified solc %s from installed=%s", selected_version, available) if not selected_version: path_ver, path_bin = _util_path_solc_info() detail = ( @@ -588,7 +575,10 @@ def run_slither(source_code: str, contract_name: str = "Contract.sol", *, requir if api_result is not None and api_result.get("success"): return api_result if api_result is not None and api_result.get("error"): - logger.warning("Python API path failed: %s — falling back to CLI", api_result.get("error")) + # The Python API is the preferred path on Render. Do not re-enter + # the CLI parser after an API compile error (argparse incompatibility). + logger.warning("Python API path failed; returning its compile result without CLI fallback: %s", api_result.get("error")) + return api_result try: proc, run_err = _run_slither_process(sol_file, out_file, solc_bin, tmp_path) From 96d18ee57ce8da4b8d2ba97ed4351c85b7c12879 Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:38:07 +0100 Subject: [PATCH 03/19] Correct compiler discovery and resolution escaping --- solc_util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/solc_util.py b/solc_util.py index a649ca1..d96047d 100644 --- a/solc_util.py +++ b/solc_util.py @@ -166,12 +166,12 @@ def installed_solc_versions() -> List[str]: if artifacts.is_dir(): for item in artifacts.glob("solc-*"): version = item.name.removeprefix("solc-") - if re.fullmatch(r"\\d+\\.\\d+\\.\\d+", version): + if re.fullmatch(r"\d+\.\d+\.\d+", version): candidates.add(version) # solc-select inventory is discovery metadata, not proof of a usable binary. candidates.update(solc_select_installed_versions()) path_version, path_binary = path_solc_info() - if path_binary and path_version and re.fullmatch(r"\\d+\\.\\d+\\.\\d+", path_version): + if path_binary and path_version and re.fullmatch(r"\d+\.\d+\.\d+", path_version): candidates.add(path_version) verified = [version for version in candidates if solc_artifact_path(version)] return sorted(set(verified), key=Version) @@ -215,7 +215,7 @@ def install_solc(version: str) -> Optional[Path]: def extract_solc_version(source_code: str, *, require_pragma: bool = False) -> Optional[str]: """Choose the highest verified installed compiler compatible with the pragma.""" - match = re.search(r"pragma\\s+solidity\\s+([^;]+);", source_code) + match = re.search(r"pragma\s+solidity\s+([^;]+);", source_code) if require_pragma and not match: return None available = installed_solc_versions() From 98ed264957e42586bbe3aa982542148521a05a0c Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:38:10 +0100 Subject: [PATCH 04/19] Correct one-time compiler mismatch retry escaping --- slither_runner.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/slither_runner.py b/slither_runner.py index 8f579de..6f08225 100644 --- a/slither_runner.py +++ b/slither_runner.py @@ -365,8 +365,8 @@ def _init_slither(path: Path, kw: dict): logger.warning("Slither() failed: %s", err_text) # A host can report either the missing requested compiler or the # only compiler it has. Retry once, but only with a verified binary. - missing_match = re.search(r"solc\\s+([0-9]+\\.[0-9]+\\.[0-9]+)\\s+not installed", err_text, re.I) - available_match = re.search(r"only\\s+([0-9]+\\.[0-9]+\\.[0-9]+)\\s+available", err_text, re.I) + missing_match = re.search(r"solc\s+([0-9]+\.[0-9]+\.[0-9]+)\s+not installed", err_text, re.I) + available_match = re.search(r"only\s+([0-9]+\.[0-9]+\.[0-9]+)\s+available", err_text, re.I) try: available = sorted(set(_util_installed_solc_versions()), key=Version) except Exception: @@ -390,10 +390,10 @@ def _init_slither(path: Path, kw: dict): try: original = sol_file.read_text(encoding="utf-8") pinned = f"pragma solidity {retry_version};" - if re.search(r"pragma\\s+solidity\\s+[^;]+;", original): - fixed = re.sub(r"pragma\\s+solidity\\s+[^;]+;", pinned, original, count=1) + if re.search(r"pragma\s+solidity\s+[^;]+;", original): + fixed = re.sub(r"pragma\s+solidity\s+[^;]+;", pinned, original, count=1) else: - fixed = pinned + "\\n" + original + fixed = pinned + "\n" + original sol_file.write_text(fixed, encoding="utf-8") alt_kwargs = dict(kwargs) alt_kwargs["solc"] = str(retry_bin) From 0e922b700dd0fe1401f813d06a1df017efe1d072 Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:39:00 +0100 Subject: [PATCH 05/19] Set production compiler default to Solidity 0.8.38 --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 46914cf..a385518 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ ENABLE_SLITHER=true \ - SOLC_VERSION=0.8.20 \ + SOLC_VERSION=0.8.38 \ PORT=8000 \ PATH="/home/appuser/.local/bin:$PATH" \ SOLC_SELECT_INSTALL_DIR=/home/appuser/.solc-select @@ -31,15 +31,15 @@ RUN pip install --upgrade pip \ # Install compilers as appuser so artifacts land under SOLC_SELECT_INSTALL_DIR USER appuser -RUN solc-select install 0.8.0 0.8.4 0.8.7 0.8.10 0.8.13 0.8.16 0.8.17 0.8.19 0.8.20 0.8.24 0.8.26 \ - && solc-select use 0.8.20 \ +RUN solc-select install 0.8.4 0.8.7 0.8.10 0.8.13 0.8.16 0.8.17 0.8.19 0.8.20 0.8.24 0.8.26 0.8.38 \ + && solc-select use 0.8.38 \ && solc --version \ && which solc \ && slither --version \ && echo "=== solc-select artifacts ===" \ && ls -la /home/appuser/.solc-select/artifacts/ \ && (find /home/appuser/.solc-select -type f -name 'solc*' | head -40) \ - && test -e /home/appuser/.solc-select/artifacts/solc-0.8.20 \ + && test -e /home/appuser/.solc-select/artifacts/solc-0.8.38 \ -o -e /home/appuser/.solc-select/artifacts/solc-0.8.20/solc-0.8.20 \ -o -e /home/appuser/.solc-select/artifacts/solc-0.8.20/solc From 3ff528bc8af671a4595d4bb7dea2a946032f2424 Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:39:03 +0100 Subject: [PATCH 06/19] Align health fallback compiler version with 0.8.38 --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index e0227cf..860c95e 100644 --- a/main.py +++ b/main.py @@ -52,7 +52,7 @@ def public_entry_points(source_code: str) -> list: except Exception: def _solc_status(): return { - "default_version": os.getenv("SOLC_VERSION", "0.8.20"), + "default_version": os.getenv("SOLC_VERSION", "0.8.38"), "versions": [], "version_count": 0, "default_binary_found": False, From cb16f1d6858b10e06c3b92af39a4530dd44493b2 Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:39:05 +0100 Subject: [PATCH 07/19] Remove duplicate compiler selection from solc patch shim --- solc_patch.py | 95 ++++++++------------------------------------------- 1 file changed, 15 insertions(+), 80 deletions(-) diff --git a/solc_patch.py b/solc_patch.py index af6b59b..288c718 100644 --- a/solc_patch.py +++ b/solc_patch.py @@ -1,95 +1,30 @@ -"""Monkey-patch solc path resolution at process start.""" +"""Compatibility shim for older deployments. + +Compiler discovery and selection now live exclusively in solc_util.py. +This module intentionally delegates instead of maintaining a second, +independent version-selection implementation. +""" from __future__ import annotations import logging -import os -import re from pathlib import Path -from typing import List, Optional +from typing import Optional logger = logging.getLogger("sentryvision.slither") + def apply() -> None: try: - import slither_runner as sr + import slither_runner as runner + from solc_util import extract_solc_version, solc_artifact_path except Exception as exc: - logger.warning("solc_patch: cannot import slither_runner: %s", exc) + logger.warning("solc_patch: centralized compiler helpers unavailable: %s", exc) return - def _solc_artifact_path(version: str) -> Optional[Path]: - roots: List[Path] = [] - env_root = os.getenv("SOLC_SELECT_INSTALL_DIR") - if env_root: - roots.append(Path(env_root)) - home_root = Path.home() / ".solc-select" - if home_root not in roots: - roots.append(home_root) - for extra in (Path("/usr/local/lib/solc-select"), Path("/opt/solc-select")): - if extra not in roots: - roots.append(extra) - candidates = [] - for root in roots: - art = root / "artifacts" - candidates.append(art / f"solc-{version}") - candidates.append(art / f"solc-{version}" / f"solc-{version}") - candidates.append(art / f"solc-{version}" / "solc") - for candidate in candidates: - try: - if candidate.is_file() and candidate.stat().st_size > 0: - return candidate - except OSError: - continue - return None - - def _extract_solc_version(source_code: str) -> Optional[str]: - from semantic_version import NpmSpec, Version - match = re.search(r"pragma\s+solidity\s+([^;]+);", source_code) - default = getattr(sr, "DEFAULT_SOLC", "0.8.20") - spec = match.group(1).strip() if match else default - try: - constraint = NpmSpec(spec) - except ValueError: - return None - installed = [] - roots: List[Path] = [] - env_root = os.getenv("SOLC_SELECT_INSTALL_DIR") - if env_root: - roots.append(Path(env_root)) - roots.append(Path.home() / ".solc-select") - for root in roots: - art = root / "artifacts" - if not art.is_dir(): - continue - for item in art.glob("solc-*"): - ver = item.name.removeprefix("solc-") - if re.fullmatch(r"\d+\.\d+\.\d+", ver) and _solc_artifact_path(ver): - installed.append(ver) - matches = [v for v in installed if constraint.match(Version(v))] - if matches: - return sorted(matches, key=lambda v: Version(v))[-1] - if re.fullmatch(r"\d+\.\d+\.\d+", spec): - return spec if sr._install_solc(spec) else None - preferred = [ - "0.8.26", "0.8.24", "0.8.20", "0.8.19", "0.8.17", "0.8.16", - "0.8.13", "0.8.10", "0.8.7", "0.8.4", "0.8.0", - ] - for cand in preferred: - try: - if constraint.match(Version(cand)): - path = sr._install_solc(cand) - if path: - return cand - except Exception: - continue - try: - if constraint.match(Version(default)) and sr._install_solc(default): - return default - except Exception: - pass - return None + # Keep legacy call sites working, but route them through verified helpers. + runner._solc_artifact_path = solc_artifact_path + runner._extract_solc_version = lambda source_code: extract_solc_version(source_code) + logger.info("solc_patch: delegated compiler resolution to solc_util") - sr._solc_artifact_path = _solc_artifact_path - sr._extract_solc_version = _extract_solc_version - logger.info("solc_patch: multi-layout artifact resolution applied") apply() From afcbfb8c8725038aacd5abeeced3d3140c1711ff Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:39:16 +0100 Subject: [PATCH 08/19] Report the effective installed compiler in health status --- solc_util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solc_util.py b/solc_util.py index d96047d..47ee165 100644 --- a/solc_util.py +++ b/solc_util.py @@ -268,7 +268,8 @@ def solc_status() -> Dict[str, Any]: if not default_binary and path_binary and path_version == effective_default: default_binary = path_binary return { - "default_version": DEFAULT_SOLC, + "default_version": effective_default or DEFAULT_SOLC, + "requested_default_version": DEFAULT_SOLC, "effective_default": effective_default, "versions": versions, "version_count": len(versions), From 3800c05c8f89375ec5c6792e7896037a6fd1c18f Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:39:26 +0100 Subject: [PATCH 09/19] Add regressions for verified solc discovery and F1 pinning --- test_hardening.py | 50 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test_hardening.py b/test_hardening.py index a3df982..1b204ef 100644 --- a/test_hardening.py +++ b/test_hardening.py @@ -6,6 +6,7 @@ import main import slither_runner +import solc_util def test_solc_range_selects_highest_installed_compatible_version(monkeypatch): @@ -206,3 +207,52 @@ def test_missing_retry_state_has_clear_expired_message(monkeypatch): assert response.status_code == 404 assert "expired" in response.json()["detail"].lower() assert "run the analysis again" in response.json()["detail"].lower() + + +def test_solc_select_inventory_is_not_trusted_without_binary(monkeypatch): + from pathlib import Path + monkeypatch.setattr(solc_util, "_solc_roots", lambda: []) + monkeypatch.setattr(solc_util, "solc_select_installed_versions", lambda: ["0.8.0", "0.8.38"]) + monkeypatch.setattr(solc_util, "path_solc_info", lambda: (None, None)) + monkeypatch.setattr( + solc_util, "solc_artifact_path", + lambda version: Path("/verified/solc") if version == "0.8.38" else None, + ) + assert solc_util.installed_solc_versions() == ["0.8.38"] + + +def test_solc_util_uses_highest_installed_for_caret_and_no_pragma(monkeypatch): + monkeypatch.setattr(solc_util, "installed_solc_versions", lambda: ["0.8.20", "0.8.38"]) + assert solc_util.extract_solc_version("pragma solidity ^0.8.0; contract Vault {}") == "0.8.38" + assert solc_util.extract_solc_version("contract LegacyLottery {}") == "0.8.38" + + +@pytest.mark.parametrize( + "source", + [ + "pragma solidity ^0.8.0; contract Vault {}", + "contract LegacyLottery {}", + ], +) +def test_f1_pins_source_to_verified_compiler_before_python_api(monkeypatch, source): + captured = {} + monkeypatch.setattr(slither_runner, "_installed_solc_versions", lambda: ["0.8.38"]) + monkeypatch.setattr(slither_runner, "_resolve_solc_binary", lambda version: (version, "/verified/solc-0.8.38")) + monkeypatch.setattr(slither_runner, "is_slither_available", lambda: True) + + def fake_api(sol_file, solc_bin): + captured["source"] = sol_file.read_text(encoding="utf-8") + captured["binary"] = solc_bin + return { + "success": True, "detectors": [{"check": "reentrancy-eth"}], + "filtered_detectors": [{"check": "reentrancy-eth"}], + "detector_checks": ["reentrancy-eth"], "count": 1, "filtered_count": 1, + "raw": {"via": "python_api"}, "error": None, "error_kind": None, + } + + monkeypatch.setattr(slither_runner, "_run_slither_python_api", fake_api) + result = slither_runner.run_slither(source, "Vault.sol") + assert result["success"] is True + assert result["detector_checks"] == ["reentrancy-eth"] + assert captured["binary"] == "/verified/solc-0.8.38" + assert captured["source"].startswith("pragma solidity 0.8.38;") From bbd1792cd26327188e10f8c4ae528f9297ac9d62 Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:39:35 +0100 Subject: [PATCH 10/19] Align compiler fail-closed tests with verified binary discovery --- tests/test_strict_patch_verification.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_strict_patch_verification.py b/tests/test_strict_patch_verification.py index cd4c528..417812f 100644 --- a/tests/test_strict_patch_verification.py +++ b/tests/test_strict_patch_verification.py @@ -50,7 +50,7 @@ def test_pragma_selects_highest_installed_compatible_compiler(monkeypatch): def test_missing_compatible_compiler_fails_closed_before_slither(monkeypatch): - monkeypatch.setattr(slither_runner, "_extract_solc_version", lambda _source: None) + monkeypatch.setattr(slither_runner, "_installed_solc_versions", lambda: []) def slither_must_not_run(): raise AssertionError("Slither must not run without a compatible compiler") @@ -60,7 +60,7 @@ def slither_must_not_run(): assert result["success"] is False assert result["error_kind"] == "compile_failed" - assert result["error"] == "unverified: no compatible compiler installed." + assert "no compatible compiler installed" in result["error"] def test_patch_without_pragma_fails_closed(monkeypatch): @@ -126,11 +126,11 @@ def test_patch_is_verified_only_when_all_gates_pass(monkeypatch): def test_verify_patch_surfaces_exact_missing_compiler_reason(monkeypatch): - monkeypatch.setattr(slither_runner, "_extract_solc_version", lambda _source: None) + monkeypatch.setattr(slither_runner, "_installed_solc_versions", lambda: []) result = verify_patch(ORIGINAL, original_code=ORIGINAL) assert result["patch_verified"] is False - assert result["patch_verification_details"] == "unverified: no compatible compiler installed." + assert "no compatible compiler installed" in result["patch_verification_details"] assert result["verification_error_kind"] == "compile_failed" From 435147e616e3d5bfee0e1ca1e6d99a8ad1eb3108 Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:40:06 +0100 Subject: [PATCH 11/19] Correct Docker compiler artifact assertion for 0.8.38 --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index a385518..688fa45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,8 +40,8 @@ RUN solc-select install 0.8.4 0.8.7 0.8.10 0.8.13 0.8.16 0.8.17 0.8.19 0.8.20 0. && ls -la /home/appuser/.solc-select/artifacts/ \ && (find /home/appuser/.solc-select -type f -name 'solc*' | head -40) \ && test -e /home/appuser/.solc-select/artifacts/solc-0.8.38 \ - -o -e /home/appuser/.solc-select/artifacts/solc-0.8.20/solc-0.8.20 \ - -o -e /home/appuser/.solc-select/artifacts/solc-0.8.20/solc + -o -e /home/appuser/.solc-select/artifacts/solc-0.8.38/solc-0.8.38 \ + -o -e /home/appuser/.solc-select/artifacts/solc-0.8.38/solc USER root COPY main.py slither_runner.py solc_util.py solc_patch.py index.html ./ From b77efcc12bed7de9dd80f9b75f6078a702910ccd Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:40:08 +0100 Subject: [PATCH 12/19] Test one-time Python API retry on solc mismatch --- test_hardening.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test_hardening.py b/test_hardening.py index 1b204ef..dd3203a 100644 --- a/test_hardening.py +++ b/test_hardening.py @@ -256,3 +256,35 @@ def fake_api(sol_file, solc_bin): assert result["detector_checks"] == ["reentrancy-eth"] assert captured["binary"] == "/verified/solc-0.8.38" assert captured["source"].startswith("pragma solidity 0.8.38;") + + +def test_python_api_retries_compiler_mismatch_once(monkeypatch, tmp_path): + import subprocess + import slither.slither + calls = [] + + class FakeSlither: + def __init__(self, source_path, **kwargs): + calls.append((Path(source_path).read_text(encoding="utf-8"), kwargs.get("solc"))) + if len(calls) == 1: + raise RuntimeError("solc 0.8.0 not installed, only 0.8.38 available") + + def register_detector(self, _detector): + return None + + def run_detectors(self): + return [] + + from pathlib import Path + source = tmp_path / "Legacy.sol" + source.write_text("pragma solidity ^0.8.0; contract Legacy {}", encoding="utf-8") + monkeypatch.setattr(slither.slither, "Slither", FakeSlither) + monkeypatch.setattr(slither_runner, "_util_installed_solc_versions", lambda: ["0.8.38"]) + monkeypatch.setattr(slither_runner, "_util_resolve_solc_binary", lambda version: (version, "/verified/solc-0.8.38")) + monkeypatch.setattr(subprocess, "run", lambda *a, **k: SimpleNamespace(returncode=0, stdout="", stderr="")) + + result = slither_runner._run_slither_python_api(source, "/missing/solc-0.8.0") + assert result["success"] is True + assert len(calls) == 2 + assert calls[1][0].startswith("pragma solidity 0.8.38;") + assert calls[1][1] == "/verified/solc-0.8.38" From 523efd42e588c3580a02ac706a32ef57e0cd7994 Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:40:39 +0100 Subject: [PATCH 13/19] Run CI against the production solc 0.8.38 compiler --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 534f436..9bb8179 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,8 +17,8 @@ jobs: - name: Install solc for real Slither end-to-end tests run: | pip install "solc-select>=1.0.4" - solc-select install 0.8.20 - solc-select use 0.8.20 + solc-select install 0.8.38 + solc-select use 0.8.38 solc --version - name: Run tests env: From 94903d34091e9570addeb3f7a98e0478349c3af7 Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:41:24 +0100 Subject: [PATCH 14/19] Prevent incompatible PATH compiler fallback and unify version selection --- slither_runner.py | 54 +++++++++++++++++++---------------------------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/slither_runner.py b/slither_runner.py index 6f08225..934ae82 100644 --- a/slither_runner.py +++ b/slither_runner.py @@ -76,26 +76,26 @@ def _installed_solc_versions() -> List[str]: def _extract_solc_version(source_code: str, *, require_pragma: bool = False) -> Optional[str]: - # Prefer shared util (PATH solc + solc-select artifacts) + """Select from the runner's verified installed-binary inventory.""" + match = re.search(r"pragma\\s+solidity\\s+([^;]+);", source_code) + if require_pragma and not match: + return None try: - ver = _util_extract_solc_version(source_code, require_pragma=require_pragma) - if ver: - return ver + available = sorted(set(_installed_solc_versions()), key=Version) except Exception as exc: - logger.warning("util extract_solc_version failed: %s", exc) - # Last resort: any PATH solc that satisfies the pragma - match = re.search(r"pragma\s+solidity\s+([^;]+);", source_code) - if require_pragma and not match: + logger.warning("Could not enumerate installed solc binaries: %s", exc) return None - spec = match.group(1).strip() if match else DEFAULT_SOLC - path_ver, path_bin = _util_path_solc_info() - if path_bin and path_ver: + if not available: + return None + if match: try: - if NpmSpec(spec).match(Version(path_ver)): - return path_ver + constraint = NpmSpec(match.group(1).strip()) + compatible = [version for version in available if constraint.match(Version(version))] + if compatible: + return compatible[-1] except ValueError: - pass - return None + logger.warning("Unsupported Solidity pragma: %s", match.group(1)) + return available[-1] _SOLC_LOCK = threading.Lock() @@ -220,31 +220,21 @@ def _install_solc(version: str) -> Optional[Path]: def _resolve_solc_binary(version: Optional[str]): if not version: - # Fall back to whatever is on PATH path_ver, path_bin = _util_path_solc_info() - if path_bin: - return path_ver or DEFAULT_SOLC, str(path_bin) + if path_bin and path_ver: + return path_ver, str(path_bin) return None, None try: - ver, path = _util_resolve_solc_binary(version) - if path: - return ver, path + resolved_version, resolved_path = _util_resolve_solc_binary(version) + if resolved_path and resolved_version == version: + return version, resolved_path except Exception as exc: logger.warning("util resolve_solc_binary failed: %s", exc) + # Installation is allowed only as an explicit attempt to obtain the exact + # requested version. Never relabel a different PATH compiler as this version. path = _solc_artifact_path(version) or _install_solc(version) if path: return version, str(path) - # PATH solc as last resort when version is compatible - path_ver, path_bin = _util_path_solc_info() - if path_bin and path_ver == version: - return version, str(path_bin) - if path_bin and path_ver: - try: - if Version(path_ver).major == Version(version).major: - logger.info("Using PATH solc %s for requested %s", path_ver, version) - return path_ver, str(path_bin) - except Exception: - pass return version, None From fbdd1eb5199ac17a0dcf63bce34715ead912e7df Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:41:33 +0100 Subject: [PATCH 15/19] Correct pragma matching expression --- slither_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slither_runner.py b/slither_runner.py index 934ae82..f45c315 100644 --- a/slither_runner.py +++ b/slither_runner.py @@ -77,7 +77,7 @@ def _installed_solc_versions() -> List[str]: def _extract_solc_version(source_code: str, *, require_pragma: bool = False) -> Optional[str]: """Select from the runner's verified installed-binary inventory.""" - match = re.search(r"pragma\\s+solidity\\s+([^;]+);", source_code) + match = re.search(r"pragma\s+solidity\s+([^;]+);", source_code) if require_pragma and not match: return None try: From a4d8460fcbfa67a8227e12ed193b592194f610b6 Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:41:40 +0100 Subject: [PATCH 16/19] Make fake solc fixtures executable for binary verification From 305b98b93686ab8f389279922bdf7b593c3040c7 Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:41:48 +0100 Subject: [PATCH 17/19] Make fake solc fixtures executable for binary verification --- tests/test_strict_patch_verification.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_strict_patch_verification.py b/tests/test_strict_patch_verification.py index 417812f..3058105 100644 --- a/tests/test_strict_patch_verification.py +++ b/tests/test_strict_patch_verification.py @@ -186,6 +186,7 @@ def test_solc_artifact_lookup_accepts_direct_file_layout(tmp_path, monkeypatch): artifact = root / "artifacts" / "solc-0.8.20" artifact.parent.mkdir(parents=True) artifact.write_bytes(b"fake-solc") + artifact.chmod(0o755) monkeypatch.setenv("SOLC_SELECT_INSTALL_DIR", str(root)) assert slither_runner._solc_artifact_path("0.8.20") == artifact From e5bb56b8669c4598a3d4f6007a4658d9e0118c77 Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:42:34 +0100 Subject: [PATCH 18/19] Mark temporary compiler fixture executable --- tests/test_imports_and_solc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_imports_and_solc.py b/tests/test_imports_and_solc.py index 48a9898..6717ca8 100644 --- a/tests/test_imports_and_solc.py +++ b/tests/test_imports_and_solc.py @@ -74,6 +74,7 @@ def test_resolve_solc_uses_artifact_without_solc_select_use(tmp_path, monkeypatc artifact.mkdir(parents=True) binary = artifact / f"solc-{version}" binary.write_text("fake-solc") + binary.chmod(0o755) monkeypatch.setenv("SOLC_SELECT_INSTALL_DIR", str(tmp_path)) def fail_run(*_args, **_kwargs): From 020dbe1445cf21e1bf346e33ce927cc840e34a2f Mon Sep 17 00:00:00 2001 From: Shukri AlGhunmi Date: Thu, 5 Nov 2026 00:42:36 +0100 Subject: [PATCH 19/19] Mark nested compiler fixture executable --- tests/test_strict_patch_verification.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_strict_patch_verification.py b/tests/test_strict_patch_verification.py index 3058105..ed16b00 100644 --- a/tests/test_strict_patch_verification.py +++ b/tests/test_strict_patch_verification.py @@ -196,6 +196,7 @@ def test_solc_artifact_lookup_accepts_nested_file_layout(tmp_path, monkeypatch): artifact = root / "artifacts" / "solc-0.8.20" / "solc-0.8.20" artifact.parent.mkdir(parents=True) artifact.write_bytes(b"fake-solc") + artifact.chmod(0o755) monkeypatch.setenv("SOLC_SELECT_INSTALL_DIR", str(root)) assert slither_runner._solc_artifact_path("0.8.20") == artifact