decode kernel panic from QR Code photo

· Gist

3 min read Original article ↗
#!/usr/bin/env python3 """ Scan a Linux panic QR code with ZXingReader and decompress its crash log. ./decode-kernel-panic.py screenshot.png -o panic.log """ import argparse import json from pathlib import Path import subprocess import sys from urllib.parse import parse_qs, urlsplit import zlib def decode_panic_url(url): """Decode the kernel's z parameter: 17 decimal digits per 7 little-endian bytes. Format reference: https://github.com/kdj0c/panic_report """ parts = urlsplit(url) parameters = parse_qs(parts.query, keep_blank_values=True) if "z" not in parameters: parameters = parse_qs(parts.fragment.lstrip("?"), keep_blank_values=True) if "z" not in parameters: raise ValueError("QR code URL has no compressed panic data (z parameter)") if len(parameters["z"]) != 1: raise ValueError("QR code URL has multiple z parameters") digits = parameters["z"][0] if not digits or not digits.isascii() or not digits.isdecimal(): raise ValueError("panic data must contain only decimal digits") # A final, partial group uses fewer digits and bytes. byte_counts = {3: 1, 5: 2, 8: 3, 10: 4, 13: 5, 15: 6, 17: 7} compressed = bytearray() for offset in range(0, len(digits), 17): chunk = digits[offset:offset + 17] if len(chunk) not in byte_counts: raise ValueError("invalid length of encoded panic data") try: compressed.extend(int(chunk).to_bytes(byte_counts[len(chunk)], "little")) except OverflowError as exc: raise ValueError("encoded panic data contains an out-of-range chunk") from exc return zlib.decompress(compressed) def scan_image(image): if not image.is_file(): raise ValueError(f"image does not exist or is not a file: {image}") try: result = subprocess.run( ["ZXingReader", "-formats", "QRCode", "-json", str(image.resolve())], capture_output=True, text=True, ) except FileNotFoundError as exc: raise ValueError("ZXingReader is required; install it and put it on PATH") from exc if result.returncode != 0: detail = result.stderr.strip() or "no readable QR code found" raise ValueError(f"ZXingReader failed: {detail}") if not result.stdout.strip(): raise ValueError("no readable QR code found") try: qr = json.loads(result.stdout) except json.JSONDecodeError as exc: raise ValueError("expected one QR code in ZXingReader's JSON output") from exc if not isinstance(qr, dict) or not isinstance(qr.get("Text"), str): raise ValueError("ZXingReader returned no QR code text") return qr["Text"] def main(): parser = argparse.ArgumentParser( description="Scan a Linux panic screenshot with ZXingReader and decompress its log.", epilog="Requires Python 3 and ZXingReader. Decoding runs locally without network access.", ) parser.add_argument("image", type=Path, help="screenshot or photo containing the panic QR code") parser.add_argument("-o", "--output", type=Path, help="write the log to a file instead of stdout") args = parser.parse_args() try: if args.output and args.output.resolve() == args.image.resolve(): raise ValueError("output must differ from the input image") log = decode_panic_url(scan_image(args.image)) if args.output: args.output.write_bytes(log) else: sys.stdout.buffer.write(log) except BrokenPipeError: # Allow commands such as `decode-kernel-panic screenshot.png | head`. import os os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno()) return 0 except (OSError, ValueError, zlib.error) as exc: print(f"{parser.prog}: {exc}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": sys.exit(main())