unsafe { tos(buf, len) } creates a non-owning string that points into an external buffer. Under autofree, local variables holding tos() strings get string.free() called on them, which calls free(s.str) on a pointer that was never allocated by malloc — undefined behavior (typically SIGABRT or SIGSEGV).
Root cause
File: vlib/builtin/string.v
tos() at line 104 creates a string without setting is_lit, so it defaults to is_lit: 0:
@[unsafe]
pub fn tos(s &u8, len int) string {
if s == 0 {
panic('tos(): nil string')
}
return string{
str: unsafe { s }
len: len
}
}
The struct definition at line 50 documents the semantics:
// .is_lit == 0 => a fresh string, should be freed by autofree
// .is_lit == 1 => a literal string from .rodata, should NOT be freed
// .is_lit == -98761234 => already freed string, protects against double frees.
string.free() at line 2288 only skips is_lit == 1 or str == 0:
@[manualfree; unsafe]
pub fn (s &string) free() {
$if prealloc {
return
}
if s.is_lit == -98761234 {
// ... double free protection ...
return
}
if s.is_lit == 1 || s.str == 0 {
return
}
unsafe {
free(s.str) // frees external memory!
s.str = nil
}
s.is_lit = -98761234
}
tos() creates a string pointing to external memory with is_lit = 0 (default). Autofree calls string.free() on it, which tries to free() a non-heap pointer. This crashes or corrupts the heap.
vstring_with_len() at line 202 and vstring() at line 218 also set is_lit: 0 for the same pattern — they all create non-owning string views that autofree will incorrectly try to free.
Verification
v run repro_tos.v # GC mode — works fine
v -autofree run repro_tos.v # autofree — SIGABRT
Reproducer (repro_tos.v)
module main
fn extract_name(json string) string {
buf := json.str
mut start := 0
mut len_ := 0
for i := 0; i < json.len - 7; i++ {
if json[i..i + 7] == '"name":' {
mut j := i + 7
for j < json.len && json[j] != `"` { j++ }
j++
start = j
for j < json.len && json[j] != `"` { j++ }
len_ = j - start
break
}
}
return unsafe { tos(buf + start, len_) }
}
fn main() {
s := '{"name":"Alice","age":30}'
name := extract_name(s)
println('name: ${name}')
println('after name: still alive')
}
unsafe { tos(buf, len) }creates a non-owning string that points into an external buffer. Under autofree, local variables holdingtos()strings getstring.free()called on them, which callsfree(s.str)on a pointer that was never allocated bymalloc— undefined behavior (typically SIGABRT or SIGSEGV).Root cause
File:
vlib/builtin/string.vtos()at line 104 creates a string without settingis_lit, so it defaults tois_lit: 0:The struct definition at line 50 documents the semantics:
string.free()at line 2288 only skipsis_lit == 1orstr == 0:tos()creates a string pointing to external memory withis_lit = 0(default). Autofree callsstring.free()on it, which tries tofree()a non-heap pointer. This crashes or corrupts the heap.vstring_with_len()at line 202 andvstring()at line 218 also setis_lit: 0for the same pattern — they all create non-owning string views that autofree will incorrectly try to free.Verification
Reproducer (
repro_tos.v)