nomen-lang 0.6.8 → 0.6.10
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 +188 -0
- package/core/System/StringBuilder.nm +10 -1
- package/core/System/Test.nm +18 -0
- package/dist/index.mjs +354 -191
- package/package.json +3 -2
- package/src/args.ts +7 -2
- package/src/index.ts +15 -4
- package/src/init.ts +10 -0
- package/src/test.ts +155 -55
package/audit_runtime.c
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#include <stdio.h>
|
|
2
|
+
#include <stdlib.h>
|
|
3
|
+
#include <string.h>
|
|
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
|
+
|
|
33
|
+
// Atomic: worker threads from the pool malloc/free concurrently, and a
|
|
34
|
+
// plain `long` would lose updates under that contention (showing up as
|
|
35
|
+
// spurious LEAK/negative-count failures in tests that use spawns).
|
|
36
|
+
static volatile long nomen_malloc_count = 0;
|
|
37
|
+
|
|
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;
|
|
60
|
+
__atomic_add_fetch(&nomen_malloc_count, 1, __ATOMIC_SEQ_CST);
|
|
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);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
void *nomen_calloc_wrap(unsigned long count, unsigned long size) {
|
|
106
|
+
unsigned long total = count * size;
|
|
107
|
+
void *ptr = audit_alloc(total);
|
|
108
|
+
if (ptr) memset(ptr, 0, total);
|
|
109
|
+
return ptr;
|
|
110
|
+
}
|
|
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.
|
|
129
|
+
void *nomen_realloc_wrap(void *old_ptr, unsigned long size) {
|
|
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;
|
|
136
|
+
}
|
|
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);
|
|
143
|
+
return ptr;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
void nomen_free_wrap(void *ptr) {
|
|
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;
|
|
161
|
+
}
|
|
162
|
+
audit_release(header);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
void nomen_audit_check(void) {
|
|
166
|
+
long count = __atomic_load_n(&nomen_malloc_count, __ATOMIC_SEQ_CST);
|
|
167
|
+
if (count != 0) {
|
|
168
|
+
printf("LEAK: %ld allocation(s)\n", count);
|
|
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);
|
|
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
|
+
}
|
|
@@ -241,7 +241,10 @@ pub struct StringBuilder {
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
// Finalize: null-terminate, return the byte buffer as a string, and
|
|
244
|
-
// release ownership (sets `data` to 0 so the destructor won't free it
|
|
244
|
+
// release ownership (sets `data` to 0 so the destructor won't free it,
|
|
245
|
+
// and resets `len`/`cap` so the builder is a valid empty builder again —
|
|
246
|
+
// a stale `cap` would let a later append skip the grow and memcpy to a
|
|
247
|
+
// null buffer). Reuse after `to_string` yields a fresh builder.
|
|
245
248
|
// Explicitly `pub`: struct members default to pub, but this is the
|
|
246
249
|
// builder's primary read-back — the contract should not depend on the
|
|
247
250
|
// default.
|
|
@@ -260,6 +263,8 @@ pub struct StringBuilder {
|
|
|
260
263
|
((unsigned char*)(unsigned long long)self->data)[idx] = 0;
|
|
261
264
|
unsigned long long result = self->data;
|
|
262
265
|
self->data = 0;
|
|
266
|
+
self->len = 0;
|
|
267
|
+
self->cap = 0;
|
|
263
268
|
return (nomen_string){ (char*)result, (long)idx };
|
|
264
269
|
```
|
|
265
270
|
```
|
|
@@ -290,6 +295,10 @@ pub struct StringBuilder {
|
|
|
290
295
|
strb wzr, [x0, x1] // data[len] = 0
|
|
291
296
|
// transfer ownership: clear self->data. x0 retains the old ptr.
|
|
292
297
|
str xzr, [x19, #8]
|
|
298
|
+
// reset the stale bookkeeping so reuse yields a valid empty builder
|
|
299
|
+
// (a stale cap would skip the append's grow against the null buffer)
|
|
300
|
+
str xzr, [x19, #16]
|
|
301
|
+
str xzr, [x19, #24]
|
|
293
302
|
// Fat-string ABI: (x0 = data, x1 = len) is already the return pair.
|
|
294
303
|
```
|
|
295
304
|
}
|
package/core/System/Test.nm
CHANGED
|
@@ -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) {
|