Skip to content

-gc e generated free fns dereference nil-default reference fields — no nil guard (SIGSEGV); root cause of cx-private#737 #2

Description

@eptx

V fork: -gc e generated free fns dereference reference fields with unsafe { nil } defaults — no nil guard (SIGSEGV)

Problem

Under -gc e, cgen's generated <T>_free functions call the pointee's free function through a reference field unconditionally, even when the field's default is unsafe { nil } and the pointee type contains heap-bearing fields (so a free fn is generated for it). Freeing any value whose reference field is still nil dereferences NULL.

Generated C (from the repro below):

static void p5__ElementMeta_free(p5__ElementMeta* it) {
	if (it->id.state != 2) {                       // it == NULL here
		builtin__string_free((string*)&(it->id.data));
	}
}

static void p5__Element_free(p5__Element* it) {
	builtin__string_free(&(it->name));
	p5__ElementMeta_free((it->meta));              // no nil guard; meta defaults to unsafe { nil }
}

Minimal repro (pure V, 2 files)

m.v:

module p5

pub struct ElementMeta {
pub mut:
	id ?string
}

pub struct Element {
pub mut:
	name string
	meta &ElementMeta = unsafe { nil }
}

pub struct Document {
pub mut:
	elements []Element
}

pub struct ParseResult {
pub mut:
	multi ?[]Document
}

pub fn parse(src string) ParseResult {
	mut docs := []Document{}
	for part in src.split('---') {
		docs << Document{
			elements: [Element{
				name: part
			}]
		}
	}
	return ParseResult{
		multi: docs
	}
}

m_test.v:

module p5

fn test_field_unwrap() {
	res := parse('a---b')
	multi := res.multi or { panic('none') }
	assert multi.len == 2
}

Run: v -cc cc -gc e test <dir> → SIGSEGV in Element_free (via Array_Document_free on the option-unwrap copy at test scope exit).

Boundary probes (all verified)

  • meta: &ElementMeta{} (allocated) instead of nil → green. So this is a nil deref, not a double-free.
  • ElementMeta { id int } (pointee needs no free fn) → green — no free call emitted through the pointer.
  • Same code, -gc boehm → green (no free insertion).
  • The option-field unwrap (res.multi or {}) is the trigger context (it's where a free of the unwrap copy gets inserted in module-internal v test compilation); returning ?[]Document or []Document directly from a fn does not insert the free and stays green. The defect itself is in the emitted free fn, which is wrong for ANY context that frees such a value.

Field impact (cx-private)

cx's Element carries two such fields (meta &ElementMeta = unsafe { nil }, table &TableData = unsafe { nil }) and nearly all parsed elements leave both nil. cx-private#737 (module-internal v test segfault on multi-doc parse) is this defect — previously misclassified as an option-unwrap double-free; the minimization disproved that. cx-private#749 (crash freeing a held []cx.Element under -gc e, ASAN-clean under -gc none) crashes in the same Element_free walk and plausibly shares this root cause — to be re-verified once the guard lands.

Fix direction

cgen free-fn emission: guard the call through any reference-typed field with a possible nil value — if (it->meta != 0) { ElementMeta_free(it->meta); }. (Whether refcount bookkeeping for the pointee also needs the guard should be checked in the same pass.)

CX-agnostic; candidate for the next upstream bundle (precedent: vlang#27458, vlang#27658).

Note

You can use the 👍 reaction to increase the issue's priority for developers.

Please note that only the 👍 reaction to the issue itself counts as a vote.
Other reactions and those to comments will not be taken into account.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions