ProgramLifetime in sanitized builds: skip the RC audit's leak report and scan instances as LeakSanitizer roots

Program-lifetime instances are never freed, and neither is anything they hold, so the RC audit of --sanitize builds reported every one of them, and every string, array and map they reference, as live at exit (4633-contract-program-lifetime failed the sanitized differential lane: 306 strings, 15 arrays, 338 objects). Like a program that abandons fibers, a program that keeps program-lifetime objects now skips the leak report with "scriptc RC audit skipped: N program-lifetime object(s) never freed", which the harnesses already filter.

Sanitized constructors call scr_obj_lifetime_note(obj), which counts the instance and registers it with LeakSanitizer as an intentional root, as publication does for published objects: once the audit no longer exits early, LeakSanitizer would otherwise report the instances the program drops. Plain builds emit nothing.
This commit is contained in:
Malte Ubl
2026-10-11 17:07:03 -07:00
parent 5bfe1a6bfd
commit 40a2c2adf2
5 changed files with 39 additions and 0 deletions
@@ -123,7 +123,12 @@ test("ProgramLifetime hierarchies are immortal from construction and take no ref
expect(nw, cls).toContain("store i64 -1, ptr %o");
expect(nw, cls).not.toContain("scr_cyc_alloc");
expect(llvm, cls).not.toContain(`@${mangleClassTrace(cls)}.cyctype`);
// The RC audit (sanitize builds) learns that the program keeps
// program-lifetime objects; other builds emit nothing for it.
expect(nw, cls).toContain("call void @scr_obj_lifetime_note(ptr %o)");
}
const plain = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false });
expect(plain).not.toContain("scr_obj_lifetime_note");
// The mortal Owner keeps its header, counts and possible-root step.
expect(body(mangleClassNew("Owner"))).toContain("store i64 1, ptr %o");
expect(body(mangleClassNew("Owner"))).toContain("scr_cyc_alloc");
@@ -632,6 +632,13 @@ export function emitClassShapes(
),
);
nw.push(` store ${host.sizeType} ${lifetime ? -1 : 1}, ptr %o`);
if (lifetime && audit) {
// --sanitize builds: the RC audit skips its leak report when the
// program keeps program-lifetime objects (scr_console.c), and
// LeakSanitizer scans them as roots (scr_object.c).
host.declare(`declare void @scr_obj_lifetime_note(ptr)`);
nw.push(` call void @scr_obj_lifetime_note(ptr %o)`);
}
if (meta.hierarchy) {
nw.push(
` %vtp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 1`,
+10
View File
@@ -43,6 +43,7 @@ extern long scr_map_live_count(void); /* scr_map.c */
extern long scr_box_live_count(void); /* scr_closure.c */
extern long scr_closure_live_count(void); /* scr_closure.c */
extern long scr_obj_live_count(void); /* scr_object.c */
extern long scr_obj_lifetime_count(void); /* scr_object.c */
extern long scr_union_live_count(void); /* scr_union.c */
extern long scr_dyn_live_count(void); /* scr_json.c */
extern long scr_bytes_live_count(void); /* scr_bytes.c */
@@ -59,6 +60,15 @@ static void scr_rc_audit_at_exit(void) {
scr_abandoned_fibers);
return;
}
/* ProgramLifetime instances (@scriptc/types) are never freed, and neither
* is anything they hold: a strict audit would only report that deliberate
* retention. */
long lifetime = scr_obj_lifetime_count();
if (lifetime > 0) {
fprintf(stderr, "scriptc RC audit skipped: %ld program-lifetime object(s) never freed\n",
lifetime);
return;
}
long strings = scr_str_live_count();
long arrays = scr_arr_live_count();
long maps = scr_map_live_count();
+12
View File
@@ -87,12 +87,24 @@ void scr_record_key_miss(ScrStr *k) {
#ifdef SCR_RC_AUDIT
static SCR_TL long scr_live_objects = 0;
/* ProgramLifetime instances (@scriptc/types) this thread created: never
* freed by design, and neither is anything they hold (see the exit audit in
* scr_console.c). */
static SCR_TL long scr_lifetime_objects = 0;
long scr_obj_live_count(void) { return scr_live_objects; }
long scr_obj_lifetime_count(void) { return scr_lifetime_objects; }
void scr_obj_alloc_note(void) { scr_live_objects++; }
void scr_obj_free_note(void) { scr_live_objects--; }
/* Also an intentional LeakSanitizer root, like published objects: what the
* instance holds stays reachable. */
void scr_obj_lifetime_note(const void *obj) {
scr_lifetime_objects++;
scr_lsan_immortal(obj);
}
#else
#undef scr_obj_alloc_note
#undef scr_obj_free_note
void scr_obj_alloc_note(void) {}
void scr_obj_free_note(void) {}
void scr_obj_lifetime_note(const void *obj) { (void)obj; }
#endif
+5
View File
@@ -5416,8 +5416,13 @@ long scr_dyn_live_count(void);
*/
void scr_obj_alloc_note(void);
void scr_obj_free_note(void);
/* Emitted by the RC-audit lane's ProgramLifetime constructors, after the
* allocation note: such instances are never freed (the exit audit skips its
* leak report, and LeakSanitizer scans them as roots). */
void scr_obj_lifetime_note(const void *obj);
#ifdef SCR_RC_AUDIT
long scr_obj_live_count(void);
long scr_obj_lifetime_count(void);
#else
/* Only the RC audit lane counts live objects. Other runtimes keep the
* out-of-line no-op symbols for direct-emission objects that still call