Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
check_alloc_pairing.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Allocator/deallocator pairing: dt_alloc_align* must be freed by dt_free_align, and
3nothing else may be.
4
5WHY THIS EXISTS, and why a build cannot replace it. dt_alloc_align() is _aligned_malloc()
6on Windows and posix_memalign() everywhere else; dt_free_align() is _aligned_free() on
7Windows and g_free() everywhere else. So on Linux and macOS BOTH families end at free(),
8every mismatch works perfectly, and no test, sanitizer or review on those platforms will
9ever see one. On Windows the same code corrupts the heap, and it crashes somewhere else
10entirely, later, in whatever unlucky code touches the heap next.
11
12That is a bug class you cannot find by running the program on the machine you develop on,
13which is what makes it worth a static check. Both directions are reported:
14
15 * something from malloc/calloc/g_malloc/g_new/strdup freed with dt_free_align()
16 * something from dt_alloc_align()/dt_calloc_align() freed with free()/g_free()/dt_free()
17
18Scope: matches allocations and frees of the SAME expression within the SAME file. That is
19deliberately narrow -- matching by name across files reports `data`, `buffer` and `img` in
20unrelated translation units and buries the real findings. It cannot see a struct field
21allocated in one file and freed in another; the generic-destructor case that produced the
22original bug report is prevented in the API instead (see dt_cache_seed in caches/cache.h),
23which is the better fix where it is available.
24
25Usage: python3 tools/check_alloc_pairing.py [--quiet]
26Exits non-zero if any mismatch is found.
27"""
28import re, os, collections
29
30ALIGNED_ALLOC = re.compile(r'\b(dt_alloc_align\w*|dt_calloc_align\w*|dt_alloc_perthread\w*|dt_realloc_align\w*)\s*\‍(')
31PLAIN_ALLOC = re.compile(r'(?<![_\w])(malloc|calloc|realloc|strdup|strndup|g_malloc\d*|g_try_malloc\d*|g_realloc|g_new\d*|g_strdup\w*|g_slice_new\w*)\s*\‍(')
32ALIGNED_FREE = re.compile(r'\bdt_free_align(?:_ptr)?\s*\‍(\s*([^,;)]*)')
33PLAIN_FREE = re.compile(r'(?<![_\w])(free|g_free|dt_free|dt_free_gpointer)\s*\‍(\s*([^,;)]*)')
34ASSIGN = re.compile(r'([A-Za-z_][\w\.\->\[\]\s]*?)\s*=\s*(?:\‍([^)]*\‍)\s*)*$')
35
36def norm(expr):
37 e = expr.strip()
38 e = re.sub(r'^\‍(+\s*|\s*\‍)+$', '', e)
39 e = re.sub(r'\‍(\s*[\w\s]*\*+\s*\‍)', '', e) # drop casts
40 e = e.strip().lstrip('&').strip()
41 e = re.sub(r'\[[^\]]*\]', '[]', e) # normalise indices
42 e = re.sub(r'\s+', '', e)
43 return e
44
45files = []
46for root, dirs, fs in os.walk('src'):
47 if 'external' in root.split(os.sep): continue
48 for f in fs:
49 if f.endswith(('.c','.cc','.h')): files.append(os.path.join(root,f))
50
51findings = []
52for path in sorted(files):
53 if path.endswith('system/mem_alloc.h'): continue
54 try: lines = open(path, encoding='utf-8', errors='replace').read().split('\n')
55 except Exception: continue
56 A = collections.defaultdict(list); P = collections.defaultdict(list)
57 AF = collections.defaultdict(list); PF = collections.defaultdict(list)
58 for i, ln in enumerate(lines, 1):
59 code = re.sub(r'//.*$', '', ln)
60 st = code.strip()
61 if st.startswith('*') or st.startswith('/*') or st.startswith('#'): continue
62 for rx, b in ((ALIGNED_ALLOC, A), (PLAIN_ALLOC, P)):
63 m = rx.search(code)
64 if m:
65 a = ASSIGN.search(code[:m.start()])
66 if a:
67 k = norm(a.group(1))
68 if k and not k.endswith('='): b[k].append((i, ln.strip()[:120]))
69 m = ALIGNED_FREE.search(code)
70 if m:
71 k = norm(m.group(1))
72 if k: AF[k].append((i, ln.strip()[:120]))
73 for m in PLAIN_FREE.finditer(code):
74 k = norm(m.group(2))
75 if k: PF[k].append((i, ln.strip()[:120]))
76 for k in set(P) & set(AF):
77 findings.append(("PLAIN alloc -> dt_free_align", path, k, P[k], AF[k]))
78 for k in set(A) & set(PF):
79 findings.append(("ALIGNED alloc -> plain free", path, k, A[k], PF[k]))
80
81import sys
82quiet = "--quiet" in sys.argv
83
84if not findings:
85 if not quiet:
86 print("OK: every dt_alloc_align* is freed by dt_free_align, and nothing else is.")
87 sys.exit(0)
88
89print(f"Allocator/deallocator mismatches: {len(findings)}")
90print()
91for kind, path, k, al, fr in findings:
92 print(f"[{kind}] {path} `{k}`")
93 for i,t in al[:2]: print(f" alloc :{i} {t}")
94 for i,t in fr[:2]: print(f" free :{i} {t}")
95 print()
96print("dt_alloc_align is _aligned_malloc on Windows and dt_free_align is _aligned_free;")
97print("everywhere else both end at free(). Each of these works on Linux and corrupts the")
98print("heap on Windows, crashing later in unrelated code.")
99sys.exit(1)