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 +157 -19
- package/core/System/Console.nm +26 -0
- package/core/System/Test.nm +18 -0
- package/dist/index.mjs +395 -182
- package/package.json +1 -1
- package/src/index.ts +8 -1
- package/src/test.ts +166 -53
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
|
-
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
+
}
|
package/core/System/Console.nm
CHANGED
|
@@ -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) {
|
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) {
|