Skip to content

autofree: tos() strings freed as owning — SIGABRT/SIGSEGV on non-heap pointers #27294

@MagicFun1241

Description

@MagicFun1241

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

  1. 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.
  1. 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')
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions