From 5f67be7c07e160e7aba8b9df49881c007eb163b0 Mon Sep 17 00:00:00 2001 From: Ricardo Salveti Date: Wed, 21 Oct 2026 01:10:28 +0000 Subject: [PATCH] skills/qcom-boot-validate: run --power-cycle-cmd without a shell boot-validate.py ran the --power-cycle-cmd string through a shell (subprocess.run(..., shell=True)), which the Semgrep rule subprocess-shell-true flags as an error. Since qcom-reusable-workflows v2.0.11 the preflight Semgrep job fails on any error-severity result, so every push to main has failed the QC Preflight Checks. Pull requests stay green only because their scans are limited to the diff. Dismissing the alert, or adding a nosemgrep comment, does not help: the gate reads the local SARIF file and ignores its suppressions. Split the command with shlex and run it without a shell. Commands made of plain words and quoting behave as before; ones that need shell syntax (pipes, redirection, variable or ~ expansion) or are scripts without a #! line must now be wrapped in sh -c '...', as the script's docstring and help text and SKILL.md now say. If the command cannot be started (empty, missing binary, unbalanced quotes), log a warning and keep watching the console, as already done for a non-zero exit. Bump the skill version to 0.2 in SKILL.md and skills.json. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Ricardo Salveti --- skills.json | 2 +- skills/qcom-boot-validate/SKILL.md | 8 +++-- .../scripts/boot-validate.py | 29 ++++++++++++++----- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/skills.json b/skills.json index 8e613d0..c592e46 100644 --- a/skills.json +++ b/skills.json @@ -308,7 +308,7 @@ "homepage": "https://github.com/qualcomm-linux/qcom-linux-skills", "versions": [ { - "version": "0.1", + "version": "0.2", "path": "skills/qcom-boot-validate", "docPath": "skills/qcom-boot-validate/SKILL.md", "files": ["skills/qcom-boot-validate/**"], diff --git a/skills/qcom-boot-validate/SKILL.md b/skills/qcom-boot-validate/SKILL.md index 062bfd8..6fa5fcd 100644 --- a/skills/qcom-boot-validate/SKILL.md +++ b/skills/qcom-boot-validate/SKILL.md @@ -11,7 +11,7 @@ description: >- over ssh). Do NOT use for flashing (see qcom-flash-qdl) or for diagnosing a healthy-but-degraded system. metadata: - version: "0.1" + version: "0.2" --- # Validate a board boots over the serial console @@ -59,7 +59,11 @@ ssh "python3 - --port /dev/ttyUSB0 --password oelinux123" \ after) resetting, e.g. right after a flash completes. - `--power-cycle-cmd` runs a host command once at startup for setups that can drive board power programmatically (lab automation, smart PDU). Keep - site-specific automation in that command, not in this skill. + site-specific automation in that command, not in this skill. The command + is split shell-style and run without a shell; wrap it in `sh -c '...'` + if it needs pipes, redirection, variable or `~` expansion, or is a script + without a `#!` line. If it cannot be started, the script warns and keeps + watching the console, so the board must then be reset by hand. - Exit code `0` on `BOOT-VALIDATION: PASS`, non-zero on failure. The full console capture is mirrored to the log file (default `/tmp/qcom-boot-validate-.log`) for evidence. diff --git a/skills/qcom-boot-validate/scripts/boot-validate.py b/skills/qcom-boot-validate/scripts/boot-validate.py index 4f91be7..a0bf8d0 100755 --- a/skills/qcom-boot-validate/scripts/boot-validate.py +++ b/skills/qcom-boot-validate/scripts/boot-validate.py @@ -16,7 +16,8 @@ By default the script only watches the console: start it, then power-cycle or reset the board (or start it right after flashing). Alternatively pass --power-cycle-cmd with a host command that power-cycles the board (e.g. a -lab automation wrapper); it is run once at startup. +lab automation wrapper); it is run once at startup, without a shell (wrap it +in sh -c '...' for shell syntax). Prints "BOOT-VALIDATION: PASS ..." and exits 0 on success; prints "BOOT-VALIDATION: FAIL ..." and exits non-zero otherwise. The full console @@ -26,6 +27,7 @@ import argparse import os import re +import shlex import subprocess import sys import time @@ -77,7 +79,8 @@ def main(): ap.add_argument("--timeout", type=float, default=300.0, help="seconds to wait for the login prompt") ap.add_argument("--power-cycle-cmd", default=None, - help="optional host command that power-cycles the board; run once at startup") + help="optional host command that power-cycles the board; run once at " + "startup, without a shell (use sh -c '...' for shell syntax)") ap.add_argument("--logfile", default=None, help="console capture path (default: /tmp/qcom-boot-validate-.log)") args = ap.parse_args() @@ -183,12 +186,22 @@ def wait_for_text(needle, timeout): # 1) trigger a clean boot ------------------------------------------------- if args.power_cycle_cmd: log("power-cycling board via: %s" % args.power_cycle_cmd) - r = subprocess.run(args.power_cycle_cmd, shell=True, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - log("power-cycle rc=%d: %s" % (r.returncode, - r.stdout.decode(errors="replace").strip())) - if r.returncode != 0: - log("WARNING: power-cycle command returned non-zero; continuing to watch console") + # No shell: the command is split into argv; wrap it in sh -c '...' + # explicitly if it needs shell syntax (pipes, redirection, $VAR, ~) + # or is a script without a #! line. + try: + argv = shlex.split(args.power_cycle_cmd) + if not argv: + raise ValueError("empty command") + r = subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + except (OSError, ValueError) as e: + log("WARNING: could not run power-cycle command (%s); " + "continuing to watch console" % e) + else: + log("power-cycle rc=%d: %s" % (r.returncode, + r.stdout.decode(errors="replace").strip())) + if r.returncode != 0: + log("WARNING: power-cycle command returned non-zero; continuing to watch console") else: log("no --power-cycle-cmd given: power-cycle or reset the board now " "if it is not already booting")