nomen-lang 0.6.9 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/audit_runtime.c CHANGED
@@ -2,44 +2,164 @@
2
2
  #include <stdlib.h>
3
3
  #include <string.h>
4
4
 
5
+ // The allocation ledger behind `--audit` (`nomen test --audit`,
6
+ // `Tester.audit_count`, and the generated harness's per-test attribution).
7
+ //
8
+ // Every wrapped block carries a 16-byte header (magic, size, state) in front of
9
+ // the payload the program sees, so every wrapped free can be matched against a
10
+ // LIVE block. The header is the whole point: a bare counter cannot tell a
11
+ // correct free from an incorrect one, and both failures it used to absorb are
12
+ // ownership bugs the generated code cannot prove away.
13
+ //
14
+ // - free of a pointer this runtime never handed out (a pool/BSS/rodata
15
+ // address, a libc allocation, a stale pointer kept across a realloc) used
16
+ // to decrement the counter like any other free. That silently hid the exact
17
+ // failure the allmark aarch64 sanitize runs hit — `free()` of a non-heap
18
+ // address aborting in libmalloc with no audit-level diagnosis — and it let
19
+ // the reported count drift away from the allocator's real state, so a
20
+ // genuine leak count and a phantom one were indistinguishable.
21
+ // - a second free of the SAME block (a double free, or a stale pointer whose
22
+ // memory the allocator already recycled) decremented a second time.
23
+ // - `realloc(p, 0)` frees `p`; the old counter kept it live, so every such
24
+ // call read as one phantom allocation.
25
+ //
26
+ // A stale free no longer decrements and no longer reaches libc `free` (it may
27
+ // not be ours to free at all): it is counted in `nomen_stale_frees` and
28
+ // reported by `nomen_audit_check`, so the run fails with a diagnosis instead of
29
+ // dying inside the allocator. The allocation count itself is unchanged — one
30
+ // increment per wrapped block, one decrement per reclaimed block — so it stays
31
+ // exactly as before: the audit test contract.
32
+
5
33
  // Atomic: worker threads from the pool malloc/free concurrently, and a
6
34
  // plain `long` would lose updates under that contention (showing up as
7
35
  // spurious LEAK/negative-count failures in tests that use spawns).
8
36
  static volatile long nomen_malloc_count = 0;
9
37
 
10
- void *nomen_malloc_wrap(unsigned long size) {
11
- void *ptr = malloc(size);
38
+ // Frees that did not match a live wrapped block (foreign pointer, double
39
+ // free, realloc of a stale pointer). Reported at exit; never affects the
40
+ // allocation count.
41
+ static volatile long nomen_stale_frees = 0;
42
+
43
+ #define NOMEN_AUDIT_MAGIC 0x6e6f6d656e415531UL // "nomenAU1"
44
+ #define NOMEN_AUDIT_LIVE 1UL
45
+ #define NOMEN_AUDIT_DEAD 2UL
46
+
47
+ typedef struct {
48
+ unsigned long magic;
49
+ unsigned long size;
50
+ unsigned long state;
51
+ unsigned long pad; // keeps the payload 16-byte aligned
52
+ } nomen_audit_header;
53
+
54
+ static void *audit_alloc(unsigned long size) {
55
+ nomen_audit_header *header = (nomen_audit_header *)malloc(size + sizeof(nomen_audit_header));
56
+ if (!header) return 0;
57
+ header->magic = NOMEN_AUDIT_MAGIC;
58
+ header->size = size;
59
+ header->state = NOMEN_AUDIT_LIVE;
12
60
  __atomic_add_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
13
- return ptr;
61
+ return (void *)(header + 1);
62
+ }
63
+
64
+ // The header in front of `ptr`, or 0 when `ptr` is not one of ours (a libc
65
+ // allocation, a static/BSS address, or already-freed memory whose header was
66
+ // cleared).
67
+ static nomen_audit_header *audit_header(void *ptr) {
68
+ nomen_audit_header *header = (nomen_audit_header *)ptr - 1;
69
+ if (header->magic != NOMEN_AUDIT_MAGIC) return 0;
70
+ return header;
71
+ }
72
+
73
+ // Retire a block: poison the header first so a second free of the same pointer
74
+ // is recognized as stale, then hand the memory back and drop the count.
75
+ static void audit_release(nomen_audit_header *header) {
76
+ header->magic = 0;
77
+ header->state = NOMEN_AUDIT_DEAD;
78
+ __atomic_sub_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
79
+ free(header);
80
+ }
81
+
82
+ static long audit_record_stale(void) {
83
+ return __atomic_add_fetch(&nomen_stale_frees, 1, __ATOMIC_SEQ_CST);
84
+ }
85
+
86
+ // Diagnose each KIND once: a shape that double-frees per iteration would
87
+ // otherwise print thousands of identical lines. The exit-time
88
+ // AUDIT-STALE-FREE count carries the total.
89
+ static void audit_report_stale(int kind) {
90
+ static volatile int reported[2] = {0, 0};
91
+ static const char *const messages[2] = {
92
+ "audit: free of a pointer that is not a live audit allocation "
93
+ "(a foreign address, or one already freed)",
94
+ "audit: realloc of a pointer that is not a live audit allocation",
95
+ };
96
+ if (kind < 0 || kind > 1) return;
97
+ if (__atomic_exchange_n(&reported[kind], 1, __ATOMIC_SEQ_CST)) return;
98
+ fprintf(stderr, "%s\n", messages[kind]);
99
+ }
100
+
101
+ void *nomen_malloc_wrap(unsigned long size) {
102
+ return audit_alloc(size);
14
103
  }
15
104
 
16
105
  void *nomen_calloc_wrap(unsigned long count, unsigned long size) {
17
- void *ptr = calloc(count, size);
18
- __atomic_add_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
106
+ unsigned long total = count * size;
107
+ void *ptr = audit_alloc(total);
108
+ if (ptr) memset(ptr, 0, total);
19
109
  return ptr;
20
110
  }
21
111
 
112
+ void *nomen_strdup_wrap(const char *s) {
113
+ unsigned long len = strlen(s) + 1;
114
+ void *ptr = audit_alloc(len);
115
+ if (ptr) memcpy(ptr, s, len);
116
+ return ptr;
117
+ }
118
+
119
+ // realloc: the count is unchanged (one block in, one block out) — the block is
120
+ // only RETIRED, which is what fixes `realloc(p, 0)`: C lets it free `p` and
121
+ // return NULL, and the old counter left `p` live, reading as a phantom
122
+ // allocation. This implementation always returns the fresh block (a valid
123
+ // unique pointer, the other conforming option), so callers that treat a NULL
124
+ // result as "the buffer is gone" keep working.
125
+ //
126
+ // A stale or foreign `old_ptr` reallocates to NULL (realloc's failure mode)
127
+ // and is counted as a stale free: the pointer is left untouched rather than
128
+ // memcpy'd out of memory that isn't ours.
22
129
  void *nomen_realloc_wrap(void *old_ptr, unsigned long size) {
23
- if (!old_ptr) {
24
- __atomic_add_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
130
+ if (!old_ptr) return audit_alloc(size);
131
+ nomen_audit_header *header = audit_header(old_ptr);
132
+ if (!header || header->state != NOMEN_AUDIT_LIVE) {
133
+ audit_record_stale();
134
+ audit_report_stale(1);
135
+ return 0;
25
136
  }
26
- void *ptr = realloc(old_ptr, size);
137
+ unsigned long old_size = header->size;
138
+ void *ptr = audit_alloc(size);
139
+ // An allocation failure keeps the old block live (realloc's contract).
140
+ if (!ptr) return 0;
141
+ memcpy(ptr, old_ptr, old_size < size ? old_size : size);
142
+ audit_release(header);
27
143
  return ptr;
28
144
  }
29
145
 
30
146
  void nomen_free_wrap(void *ptr) {
31
- if (ptr) {
32
- free(ptr);
33
- __atomic_sub_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
147
+ if (!ptr) return;
148
+ nomen_audit_header *header = audit_header(ptr);
149
+ // Not a live wrapped block: a libc allocation, a static/BSS/rodata
150
+ // address, a pointer the allocator already recycled, or a block this
151
+ // runtime already freed (the double-free case — the header is poisoned on
152
+ // release, so a stale pointer can never look live). Freeing it would be an
153
+ // invalid free, and libmalloc aborts INSIDE free() with no context, which
154
+ // is exactly how the allmark aarch64 sanitize run died. Report it instead
155
+ // of passing it on, and leave the count alone: an unmatched free is not
156
+ // evidence that a live block was reclaimed.
157
+ if (!header || header->state != NOMEN_AUDIT_LIVE) {
158
+ audit_record_stale();
159
+ audit_report_stale(0);
160
+ return;
34
161
  }
35
- }
36
-
37
- void *nomen_strdup_wrap(const char *s) {
38
- unsigned long len = strlen(s) + 1;
39
- void *ptr = malloc(len);
40
- if (ptr) memcpy(ptr, s, len);
41
- __atomic_add_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
42
- return ptr;
162
+ audit_release(header);
43
163
  }
44
164
 
45
165
  void nomen_audit_check(void) {
@@ -47,4 +167,22 @@ void nomen_audit_check(void) {
47
167
  if (count != 0) {
48
168
  printf("LEAK: %ld allocation(s)\n", count);
49
169
  }
170
+ long stale = __atomic_load_n(&nomen_stale_frees, __ATOMIC_SEQ_CST);
171
+ if (stale != 0) {
172
+ printf("AUDIT-STALE-FREE: %ld pointer(s) freed that were not live audit allocations\n",
173
+ stale);
174
+ }
175
+ }
176
+
177
+ // Per-test leak attribution (`nomen test`): the generated harness snapshots
178
+ // the counter before every test and diffs it after, so a leaking test is
179
+ // named instead of a whole file aggregating into one exit-time count.
180
+ long nomen_audit_count(void) {
181
+ return __atomic_load_n(&nomen_malloc_count, __ATOMIC_SEQ_CST);
50
182
  }
183
+
184
+ // Frees that matched no live block (foreign pointer / double free). Polled the
185
+ // same way as the count so a run can attribute them like leaks.
186
+ long nomen_audit_stale_frees(void) {
187
+ return __atomic_load_n(&nomen_stale_frees, __ATOMIC_SEQ_CST);
188
+ }
@@ -43,6 +43,32 @@ pub struct Console {
43
43
  ```
44
44
  }
45
45
 
46
+ // Disable stdout buffering: every write goes straight to the file
47
+ // descriptor. The `nomen test` harness calls this at startup so a
48
+ // crashing binary still leaves every record it printed before the
49
+ // crash — a buffered stdout loses its pending bytes on an abnormal
50
+ // exit, which used to turn a mid-suite abort into "no records at all".
51
+ pub func unbuffered = () {
52
+ ```
53
+ #arch: c
54
+ setbuf(stdout, NULL);
55
+ ```
56
+ ```
57
+ #arch: aarch64
58
+ // setbuf(stdout, NULL): Apple's stdio names the FILE* global
59
+ // __stdoutp (stdout itself is a macro), so the Mach-O symbol is
60
+ // ___stdoutp — and it must load through the GOT (@GOTPAGE), the
61
+ // only relocation the linker accepts for it. The aarch64 backend
62
+ // always emits Mach-O symbol names (see `bl _printf` above), so
63
+ // this is unconditional like every other core raw body.
64
+ adrp x0, ___stdoutp@GOTPAGE
65
+ ldr x0, [x0, ___stdoutp@GOTPAGEOFF]
66
+ ldr x0, [x0]
67
+ mov x1, #0
68
+ bl _setbuf
69
+ ```
70
+ }
71
+
46
72
  // Read one line from stdin (newline stripped). The raw body mallocs the
47
73
  // buffer — `move out string` transfers ownership to the caller.
48
74
  pub func read_line = (move out string) {
@@ -11,6 +11,7 @@
11
11
  //
12
12
  // \nomen|start|<test>
13
13
  // \nomen|fail|<test>|<message>
14
+ // \nomen|leaks|<test>|<n>
14
15
  // \nomen|done|<test>|<passed>|<failed>|<ns>
15
16
  // \nomen|bench|<label>|<n>|<min>|<median>|<max>|<mean>|<stddev>
16
17
  //
@@ -70,6 +71,23 @@ pub class Tester {
70
71
  var bench_label = ""
71
72
  var bench_samples = 1000
72
73
 
74
+ // (audit) The process's current malloc/free balance — allocations made
75
+ // but not yet freed. `nomen test --audit` snapshots this around every
76
+ // test so a leak is attributed to the test that leaked instead of one
77
+ // file-level exit-time count. The audit runtime is linked into every
78
+ // test build; in other binaries the counter simply never moves.
79
+ pub func audit_count = (out int64) {
80
+ ```
81
+ #arch: c
82
+ extern long nomen_audit_count(void);
83
+ return (long long)nomen_audit_count();
84
+ ```
85
+ ```
86
+ #arch: aarch64
87
+ bl _nomen_audit_count
88
+ ```
89
+ }
90
+
73
91
  // (harness) Start timing `name` and clear the per-test state. Called by
74
92
  // the `main` that `nomen test` generates — not by test code.
75
93
  pub func begin_test = (ref self, string name) {