Skip to content

explore: symbol_index keys alias outline-owned strings — freed on re-index while shared-name entries survive (UAF) #586

@justrach

Description

@justrach

Problem

rebuildSymbolIndexFor keys the global symbol_index with sym.name slices owned by the file's outline (src/explore.zig:5081). When that file is re-indexed, commitParsedFileOwnedOutline deinits the prior outline (src/explore.zig:915), freeing the very bytes the map hashes and compares — but the map entry survives whenever any other file shares the symbol name, because removeSymbolIndexFor only drops entries whose location list becomes empty. In Zig code, init/deinit/main are shared by nearly every file, so the first edit of whichever file first inserted a shared name leaves a dangling key. The same happens on file deletion: Explorer.removeFile frees the outline (src/explore.zig:1513) after removeSymbolIndexFor kept shared-name entries alive.

Every subsequent lookup or iteration eql()s against freed memory — UB in release builds (with reused memory, potentially garbage names in symbolSearch results); under the DebugAllocator poison the entry becomes unreachable, silently degrading the O(1) index to the outline safety scans for the daemon's lifetime.

Failing Test

test_explore.zig — fails on current release tip (error.SymbolLostFromIndex):

test "issue-586: symbol_index keys must survive re-index of the file that first inserted them" {
    var explorer = Explorer.init(testing.allocator, Explorer.DEFAULT_CONTENT_CACHE_CAPACITY);
    defer explorer.deinit();

    // a.zig inserts sharedFn first -> the map key aliases a.zig's outline string.
    try explorer.indexFile("src/a.zig", "pub fn sharedFn() void {}\n");
    try explorer.indexFile("src/b.zig", "pub fn sharedFn() void {}\n");

    // Re-index the key owner: its old outline is freed; the entry stays alive
    // because b.zig still holds a location.
    try explorer.indexFile("src/a.zig", "pub fn sharedFn() void {}\npub fn other() void {}\n");

    const locs = explorer.symbol_index.get("sharedFn") orelse return error.SymbolLostFromIndex;
    try testing.expect(locs.items.len >= 2);
}

Expected

symbol_index entries stay valid and reachable regardless of which file's outline is replaced or removed.

Fix

Make the map own its keys: dupe on first insert in rebuildSymbolIndexFor, free the key when removeSymbolIndexFor drops an emptied entry, and free keys in Explorer.deinit. One dupe per unique symbol name; uniform ownership also covers the snapshot fast-load case where names are borrowed from the mmap'd outline section.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:p2Medium priority

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions