From: Breno Rodrigues Alves <breno3011alves@gmail.com>
To: linux-kernel@vger.kernel.org
Cc: Andy Whitcroft <apw@canonical.com>, Joe Perches <joe@perches.com>,
Dwaipayan Ray <dwaipayanray1@gmail.com>,
Lukas Bulwahn <lukas.bulwahn@gmail.com>,
Breno Rodrigues Alves <breno3011alves@gmail.com>
Subject: [RFC PATCH 6/7] scripts: convert leaking_addresses.pl to Python
Date: Sun, 18 Oct 2026 08:06:37 -0300 [thread overview]
Message-ID: <20261018110638.9947-7-breno3011alves@gmail.com> (raw)
In-Reply-To: <20261018110638.9947-1-breno3011alves@gmail.com>
Faithful re-implementation of leaking_addresses.pl in Python 3
(stdlib only). Same architecture detection, same false-positive
filtering, same dmesg and /proc walk.
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
scripts/leaking_addresses.py | 430 +++++++++++++++++++++++++++++++++++
1 file changed, 430 insertions(+)
create mode 100755 scripts/leaking_addresses.py
diff --git a/scripts/leaking_addresses.py b/scripts/leaking_addresses.py
new file mode 100755
index 000000000..a1664a66d
--- /dev/null
+++ b/scripts/leaking_addresses.py
@@ -0,0 +1,430 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# (c) 2017 Tobin C. Harding <me@tobin.cc>
+#
+# leaking_addresses.py: Scan the kernel for potential leaking addresses.
+#
+# Use --debug to output path before parsing.
+
+import argparse
+import os
+import platform
+import re
+import signal
+import subprocess
+import sys
+
+DIRS = ('/proc', '/sys')
+TIMEOUT = 10
+
+SKIP_ABS = {
+ '/proc/kmsg',
+ '/proc/device-tree',
+ '/proc/1/syscall',
+ '/sys/firmware/devicetree',
+ '/sys/kernel/tracing/trace_pipe',
+ '/sys/kernel/debug/tracing/trace_pipe',
+ '/sys/kernel/security/apparmor/revision',
+}
+
+SKIP_ANY = {
+ 'pagemap', 'events', 'access', 'registers', 'snapshot_raw',
+ 'trace_pipe_raw', 'ptmx', 'trace_pipe', 'fd', 'usbmon',
+}
+
+RE_ARCH_X86 = re.compile(r'i[3456]86')
+
+# Active per-arch state
+_ARCH = platform.machine() or ''
+_OPT_32BIT = False
+_PAGE_OFFSET_32BIT = 0
+_PGTABLE_LEVELS = None
+_KALLSYMS = []
+_KERNEL_CONFIG_FILE = ''
+_PAGE_OFFSET_CACHE = None
+
+
+def dprint(*args):
+ if _DEBUG:
+ print(*args, file=sys.stderr)
+
+
+def is_x86_64():
+ return _ARCH == 'x86_64'
+
+
+def is_ppc64():
+ return _ARCH == 'ppc64'
+
+
+def is_ix86_32():
+ return bool(RE_ARCH_X86.match(_ARCH))
+
+
+def is_32bit():
+ return _OPT_32BIT or bool(_PAGE_OFFSET_32BIT) or is_ix86_32()
+
+
+def get_kernel_config_option(option):
+ files = []
+ if _KERNEL_CONFIG_FILE:
+ files = [_KERNEL_CONFIG_FILE]
+ elif os.access('/proc/config.gz', os.R_OK):
+ try:
+ import gzip
+ with gzip.open('/proc/config.gz', 'rt') as f:
+ for line in f:
+ if line.startswith(option):
+ return line.split('=', 1)[1].strip()
+ except OSError as e:
+ dprint(f'failed reading /proc/config.gz: {e}')
+ return ''
+ else:
+ rel = subprocess.check_output(['uname', '-r'], text=True).strip()
+ files = [f'/boot/config-{rel}', '/boot/config']
+
+ for path in files:
+ try:
+ with open(path) as f:
+ for line in f:
+ if line.startswith(option):
+ return line.split('=', 1)[1].strip()
+ except OSError:
+ continue
+ return ''
+
+
+def get_page_offset():
+ global _PAGE_OFFSET_CACHE
+ if _PAGE_OFFSET_CACHE is not None:
+ return _PAGE_OFFSET_CACHE
+ default = 0xc0000000
+ if _PAGE_OFFSET_32BIT:
+ _PAGE_OFFSET_CACHE = _PAGE_OFFSET_32BIT
+ return _PAGE_OFFSET_CACHE
+ val = get_kernel_config_option('CONFIG_PAGE_OFFSET')
+ if val:
+ _PAGE_OFFSET_CACHE = int(val, 16)
+ return _PAGE_OFFSET_CACHE
+ _PAGE_OFFSET_CACHE = default
+ return default
+
+
+def is_false_positive_64(match):
+ if re.search(r'\b(0x)?(f|F){15}[0-9a-f]\b', match) or \
+ re.search(r'\b(0x)?0{16}\b', match):
+ return True
+ if is_x86_64() and _in_vsyscall(match):
+ return True
+ return False
+
+
+def is_false_positive_32(match):
+ if re.search(r'\b(0x)?(f|F){7}[0-9a-f]\b', match):
+ return True
+ if int(match, 16) < get_page_offset():
+ return True
+ return False
+
+
+def is_false_positive(match):
+ if is_32bit():
+ return is_false_positive_32(match)
+ return is_false_positive_64(match)
+
+
+def _in_vsyscall(match):
+ v = int(match, 16)
+ return 0xffffffffff600000 <= v <= 0xffffffffff601000
+
+
+def get_address_re():
+ if is_ppc64():
+ return re.compile(r'\b(0x)?[89abcdef]00[[:xdigit:]]{13}\b'
+ .replace('[[:xdigit:]]', r'[0-9a-fA-F]'))
+ if is_32bit():
+ return re.compile(r'\b(0x)?[0-9a-fA-F]{8}\b')
+ global _PGTABLE_LEVELS
+ if _PGTABLE_LEVELS is None:
+ val = get_kernel_config_option('CONFIG_PGTABLE_LEVELS')
+ _PGTABLE_LEVELS = int(val) if val else 0
+ if _PGTABLE_LEVELS == 5:
+ return re.compile(r'\b(0x)?ff[0-9a-fA-F]{14}\b')
+ return re.compile(r'\b(0x)?ffff[0-9a-fA-F]{12}\b')
+
+
+def may_leak_address(path, line):
+ if re.match(r'^SigBlk:', line) or re.match(r'^SigIgn:', line) \
+ or re.match(r'^SigCgt:', line):
+ return False
+ if re.search(r'\bKEY=[0-9a-fA-F]{9,14} [0-9a-fA-F]{16} '
+ r'[0-9a-fA-F]{16}\b', line):
+ return False
+ if path.endswith('key') and re.search(
+ r'\b[0-9a-fA-F]{9,14} [0-9a-fA-F]{16} [0-9a-fA-F]{16}\b',
+ line):
+ return False
+ pattern = get_address_re()
+ for m in pattern.finditer(line):
+ if not is_false_positive(m.group(0)):
+ return True
+ return False
+
+
+def parse_dmesg():
+ try:
+ out = subprocess.check_output(['dmesg'], text=True,
+ stderr=subprocess.DEVNULL)
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ return
+ for line in out.splitlines(keepends=True):
+ if may_leak_address('dmesg', line):
+ sys.stdout.write('dmesg: ' + line)
+
+
+def skip(path):
+ if path in SKIP_ABS:
+ return True
+ return os.path.basename(path) in SKIP_ANY
+
+
+def parse_binary(path):
+ try:
+ with open(path, 'rb') as f:
+ data = f.read()
+ except OSError:
+ return
+ for addr, symbol in _KALLSYMS:
+ off = data.find(addr)
+ if off != -1:
+ print(f"{path}: {symbol} @ {off}")
+
+
+def parse_file(path):
+ if not os.access(path, os.R_OK):
+ return
+ # Skip pseudo-binaries
+ if path.startswith(('/sys/kernel/btf/', '/sys/devices/pci',
+ '/sys/firmware/efi/efivars/', '/proc/bus/pci/')):
+ return
+ try:
+ with open(path, 'r', errors='replace') as f:
+ for line in f:
+ line = line.rstrip('\n')
+ if may_leak_address(path, line):
+ print(f"{path}: {line}")
+ except (OSError, UnicodeDecodeError):
+ if _KALLSYMS:
+ parse_binary(path)
+
+
+def check_path_for_leaks(path):
+ if may_leak_address(path, path):
+ print(f"Path name may contain address: {path}")
+
+
+def timed_parse_file(path):
+ def handler(signum, frame):
+ raise TimeoutError()
+ old = signal.signal(signal.SIGALRM, handler)
+ signal.alarm(TIMEOUT)
+ try:
+ parse_file(path)
+ except TimeoutError:
+ print(f"timed out parsing: {path}", file=sys.stderr)
+ finally:
+ signal.alarm(0)
+ signal.signal(signal.SIGALRM, old)
+
+
+def walk(dirs):
+ stack = list(dirs)
+ while stack:
+ pwd = stack.pop()
+ try:
+ entries = os.listdir(pwd)
+ except OSError:
+ continue
+ for name in entries:
+ if name in ('.', '..'):
+ continue
+ path = os.path.join(pwd, name)
+ if os.path.islink(path):
+ continue
+ # Skip /proc/PID except /proc/1
+ if re.match(r'^/proc/\d+$', path) and path != '/proc/1':
+ continue
+ if skip(path):
+ continue
+ check_path_for_leaks(path)
+ if os.path.isdir(path):
+ stack.append(path)
+ continue
+ dprint(f'parsing: {path}')
+ timed_parse_file(path)
+
+
+def format_output(path, raw, squash_path, squash_filename, suppress_dmesg):
+ if raw or not (squash_path or squash_filename):
+ _dump_raw(path, suppress_dmesg)
+ return
+ total, dmesg, paths, files = _parse_raw(path)
+ print(f"\nTotal number of results from scan (incl dmesg): {total}")
+ if not suppress_dmesg:
+ _print_dmesg(dmesg)
+ if squash_filename:
+ _squash_by(files, 'filename')
+ if squash_path:
+ _squash_by(paths, 'path')
+
+
+def _dump_raw(path, suppress_dmesg):
+ try:
+ with open(path) as f:
+ for line in f:
+ if suppress_dmesg and line.startswith('dmesg:'):
+ continue
+ sys.stdout.write(line)
+ except OSError as e:
+ sys.exit(f"{path}: {e}")
+
+
+def _parse_raw(path):
+ total = 0
+ dmesg = []
+ paths = {}
+ files = {}
+ try:
+ with open(path) as f:
+ for line in f:
+ total += 1
+ if line.startswith('dmesg:'):
+ dmesg.append(line)
+ continue
+ idx = line.find(': ')
+ if idx < 0:
+ continue
+ p = line[:idx]
+ v = line[idx + 2:]
+ paths.setdefault(p, []).append(v)
+ files.setdefault(os.path.basename(p), []).append(v)
+ except OSError as e:
+ sys.exit(f"{path}: {e}")
+ return total, dmesg, paths, files
+
+
+def _print_dmesg(dmesg):
+ print("\ndmesg output:")
+ if not dmesg:
+ print('<no results>')
+ return
+ for line in dmesg:
+ idx = line.find(': ')
+ sys.stdout.write(line[idx + 2:] if idx >= 0 else line)
+
+
+def _squash_by(ref, desc):
+ print(f"\nResults squashed by {desc} (excl dmesg). "
+ f"Displaying [<number of results> <{desc}>], <example result>")
+ if not ref:
+ print('<no results>')
+ return
+ for key in ref:
+ vals = ref[key]
+ print(f"[{len(vals)} {key}] {vals[0]}")
+
+
+def _load_kallsyms(path):
+ with open(path) as f:
+ for line in f:
+ parts = line.split()
+ if len(parts) < 3:
+ continue
+ addr_text = parts[0]
+ if addr_text.startswith('0'):
+ continue
+ try:
+ addr = int(addr_text, 16)
+ except ValueError:
+ continue
+ _KALLSYMS.append((addr.to_bytes(8, 'little'), parts[2]))
+
+
+def main(argv):
+ global _OPT_32BIT, _PAGE_OFFSET_32BIT, _KERNEL_CONFIG_FILE, _DEBUG
+ p = argparse.ArgumentParser(add_help=False)
+ p.add_argument('-d', '--debug', action='store_true')
+ p.add_argument('-h', '--help', action='store_true')
+ p.add_argument('-o', '--output-raw')
+ p.add_argument('-i', '--input-raw')
+ p.add_argument('--suppress-dmesg', action='store_true')
+ p.add_argument('--squash-by-path', action='store_true')
+ p.add_argument('--squash-by-filename', action='store_true')
+ p.add_argument('--raw', action='store_true')
+ p.add_argument('--kallsyms')
+ p.add_argument('--kernel-config-file')
+ p.add_argument('--32-bit', action='store_true')
+ p.add_argument('--page-offset-32-bit', type=lambda v: int(v, 0),
+ default=0)
+ args = p.parse_args(argv)
+
+ if args.help:
+ print(f"Usage: {sys.argv[0]} [OPTIONS]")
+ print()
+ print("Options:")
+ print(" -o, --output-raw=<file> Save results for future processing.")
+ print(" -i, --input-raw=<file> Read results from file instead of scanning.")
+ print(" --raw Show raw results (default).")
+ print(" --suppress-dmesg Do not show dmesg results.")
+ print(" --squash-by-path Show one result per unique path.")
+ print(" --squash-by-filename Show one result per unique filename.")
+ print(" --kernel-config-file=<file> Kernel configuration file (e.g /boot/config).")
+ print(" --kallsyms=<file> Read kernel symbol addresses from file.")
+ print(" --32-bit Scan 32-bit kernel.")
+ print(" --page-offset-32-bit=o Page offset (for 32-bit kernel).")
+ print(" -d, --debug Display debugging output.")
+ print(" -h, --help Display this help and exit.")
+ print()
+ print("Scans the running kernel for potential leaking addresses.")
+ return 0
+ _DEBUG = args.debug
+ _OPT_32BIT = args.__dict__['32_bit']
+ _PAGE_OFFSET_32BIT = args.page_offset_32_bit
+ if args.kernel_config_file:
+ _KERNEL_CONFIG_FILE = args.kernel_config_file
+
+ if args.input_raw:
+ format_output(args.input_raw, args.raw, args.squash_by_path,
+ args.squash_by_filename, args.suppress_dmesg)
+ return 0
+
+ if not (is_x86_64() or is_ppc64() or is_ix86_32()
+ or _OPT_32BIT or _PAGE_OFFSET_32BIT):
+ print("\nScript does not support your architecture, sorry.\n")
+ print("Currently we support: x86_64, ppc64, x86\n")
+ return 129
+
+ out_fd = None
+ if args.output_raw:
+ try:
+ out_fd = open(args.output_raw, 'w')
+ except OSError as e:
+ sys.exit(f"{args.output_raw}: {e}")
+ # Redirect print() calls
+ sys.stdout = out_fd
+
+ if args.kallsyms:
+ _load_kallsyms(args.kallsyms)
+
+ parse_dmesg()
+ walk(DIRS)
+
+ if out_fd:
+ out_fd.close()
+ sys.stdout = sys.__stdout__
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
next prev parent reply other threads:[~2026-10-18 11:07 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-18 11:06 [RFC PATCH 0/7] scripts: convert seven more Perl utilities " Breno Rodrigues Alves
2026-10-18 11:06 ` [RFC PATCH 1/7] scripts: convert checkdeclares.pl " Breno Rodrigues Alves
2026-10-18 11:06 ` [RFC PATCH 2/7] scripts: convert checkversion.pl " Breno Rodrigues Alves
2026-10-18 11:06 ` [RFC PATCH 3/7] scripts: convert extract_xc3028.pl " Breno Rodrigues Alves
2026-10-18 11:06 ` [RFC PATCH 4/7] scripts: convert generate_initcall_order.pl " Breno Rodrigues Alves
2026-10-18 11:06 ` [RFC PATCH 5/7] scripts: convert get_maintainer.pl " Breno Rodrigues Alves
2026-10-18 11:06 ` Breno Rodrigues Alves [this message]
2026-10-18 11:06 ` [RFC PATCH 7/7] scripts: convert recordmcount.pl " Breno Rodrigues Alves
2026-10-18 20:59 ` [RFC PATCH 0/7] scripts: convert seven more Perl utilities " Jonathan Corbet
2026-10-18 23:19 ` Breno Rodrigues Alves
2026-10-19 10:05 ` Heiko Carstens
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261018110638.9947-7-breno3011alves@gmail.com \
--to=breno3011alves@gmail.com \
--cc=apw@canonical.com \
--cc=dwaipayanray1@gmail.com \
--cc=joe@perches.com \
--cc=linux-kernel@vger.kernel.org \
--cc=lukas.bulwahn@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®