DF-2898 / sysent_xcheck.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | #!/usr/bin/env python3 """Pass-2 cross-check of sys/kern/init_sysent.c against sysproto.h / sysunion.h / syscalls.c. x86_64 model: every field in a generated args struct is followed by a PAD_ that rounds the running offset up to a multiple of sizeof(register_t)=8, and no field type exceeds 8 bytes, so: words(struct) == number of fields, sizeof(struct) == 8*nfields. """ import re, sys ROOT = "/home/maxx/dfbsd/dfbsd" sysent_src = open(f"{ROOT}/sys/kern/init_sysent.c").read() sysproto_src = open(f"{ROOT}/sys/sys/sysproto.h").read() sysunion_src = open(f"{ROOT}/sys/sys/sysunion.h").read() syscalls_src = open(f"{ROOT}/sys/kern/syscalls.c").read() # ---- parse sysent[] rows: { narg, rsize, call }, /* N = name */ row_re = re.compile( r'\{\s*(?:AS\((\w+)\)|(\d+))\s*,\s*(\d+)\s*,\s*\(sy_call_t \*\)(\w+)\s*\}' r'\s*,?\s*/\*\s*(\d+)\s*=\s*([^*]*?)\s*\*/') rows = {} for m in row_re.finditer(sysent_src): asname, lit, rsize, func, idx, name = m.groups() rows[int(idx)] = dict(asm=asname, lit=int(lit) if lit else None, rsize=int(rsize), func=func, name=name.strip()) problems = [] SYS_MAXSYSCALL = int(re.search(r'#define\s+SYS_MAXSYSCALL\s+(\d+)', open(f"{ROOT}/sys/sys/syscall.h").read()).group(1)) n_rows = len(rows) print(f"[i] sysent rows parsed: {n_rows} (0..{max(rows)}); SYS_MAXSYSCALL={SYS_MAXSYSCALL}") if n_rows != SYS_MAXSYSCALL or max(rows) != SYS_MAXSYSCALL - 1: problems.append(f"sysent length {n_rows} != SYS_MAXSYSCALL {SYS_MAXSYSCALL}") if sorted(rows) != list(range(max(rows) + 1)): missing = set(range(max(rows)+1)) - set(rows) dups = [i for i in set(rows) if list(rows).count(i) > 1] problems.append(f"index gaps/dups: missing={missing}") # ---- parse sysproto.h structs: name -> (nfields, [(type, name)], is_dummy) struct_re = re.compile(r'struct\s+(\w+)\s*\{(.*?)\n\};', re.S) field_re = re.compile(r'^\s*(?:const\s+)?([\w ]+?[\w*])\s+(\w+);') structs = {} for m in struct_re.finditer(sysproto_src): sname, body = m.group(1), m.group(2) fields = [] for line in body.splitlines(): line = line.split('/*')[0] fm = field_re.match(line) if fm and not fm.group(2).endswith('_') and fm.group(1) not in ('char',): fields.append((fm.group(1).strip(), fm.group(2))) if fields: structs[sname] = fields print(f"[i] sysproto.h structs parsed: {len(structs)}") # sanity vs known impls for probe in ('read_args', 'mmap_args', 'lseek_args', 'fork_args', 'nosys_args'): if probe in structs: print(f"[i] {probe}: {len(structs[probe])} fields -> {[f[1] for f in structs[probe]]}") # ---- parse sysunion members union_members = set(re.findall(r'struct\s+(\w+)\s+\w+;', sysunion_src)) print(f"[i] sysunion.h members: {len(union_members)}") # ---- parse syscallnames[] name_re = re.compile(r'"([^"]*)",?\s*/\*\s*(\d+)') names = {int(m.group(2)): m.group(1) for m in name_re.finditer(syscalls_src)} print(f"[i] syscallnames[] entries: {len(names)} (max {max(names)})") if len(names) != SYS_MAXSYSCALL or max(names) != SYS_MAXSYSCALL - 1: problems.append(f"syscallnames length {len(names)} != SYS_MAXSYSCALL") # ---- per-row checks maxwords = 0 for idx in sorted(rows): r = rows[idx] # name alignment (informational for alias rows) # 1. AS(x_args) struct must exist and be a sysunion member if r["asm"]: sname = r["asm"] if sname not in structs: problems.append(f"[{idx}] AS({sname}): struct not found in sysproto.h") continue words = len(structs[sname]) maxwords = max(maxwords, words) if sname not in union_members: problems.append(f"[{idx}] AS({sname}) NOT a member of union sysunion " f"(extargs copyin dest) -> potential stack overflow") # struct must be all-dummy or >0 fields ok; AS count equals words by construction # 2. literal 0 narg with a real (non-nosys/lkm) func: args struct must be dummy-only elif r["lit"] == 0 and r["func"] not in ("sys_nosys", "sys_lkmnosys"): # find the args struct the impl expects: func sys_foo -> struct foo_args (with manual fixes) base = r["func"].removeprefix("sys_") cand = f"{base}_args" # table aliases: wait4 uses wait_args etc. alias = {"wait4": "wait_args", "___sysctl": "sysctl_args", "___getcwd": "__getcwd_args", "__getrlimit": "__getrlimit_args", "__setrlimit": "__setrlimit_args", "___realpath": "__realpath_args", "xsyscall": "nosys_args"}.get(base, cand) st = structs.get(alias) if st is None: problems.append(f"[{idx}] {r['func']}: narg=0 but args struct {alias} unknown") elif not (len(st) == 1 and st[0][1] == "dummy"): problems.append(f"[{idx}] {r['func']}: narg=0 in table but args struct " f"{alias} has fields {[f[1] for f in st]} -> impl reads " f"uninitialized/absent args") # 3. rsize sanity if r["rsize"] not in (4, 8): problems.append(f"[{idx}] weird rsize {r['rsize']}") print(f"[i] max args words across table: {maxwords}") # ---- union capacity: largest member struct umax = max((len(structs[m]) for m in union_members if m in structs), default=0) print(f"[i] union sysunion capacity: {umax} words (largest member)") if maxwords > umax: problems.append(f"table max narg {maxwords} > union capacity {umax} -> " f"copyin overflows sysmsg.extargs on kernel stack") # ---- name table consistency: every implemented row's comment name matches syscallnames for idx in sorted(rows): nm = rows[idx]["name"].strip() if idx in names: pass # informational only; aliases (netbsd_*) differ by design # ---- impls with rsize==8: verify they produce 64-bit results print("\n[i] rsize==8 entries:", [ (i, rows[i]["func"]) for i in sorted(rows) if rows[i]["rsize"] == 8 ]) print("\n==== PROBLEMS ====" if problems else "\n==== NO STRUCTURAL PROBLEMS ====") for p in problems: print(" !", p) |