From 9aaa374f127a69561cf0d1b9d51b264f42bc87ae Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Mon, 6 Jul 2026 13:12:10 -0700 Subject: [PATCH] address uaf in portfolio command watchlist --- src/commands/portfolio.zig | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/commands/portfolio.zig b/src/commands/portfolio.zig index f2f7c7d..c033172 100644 --- a/src/commands/portfolio.zig +++ b/src/commands/portfolio.zig @@ -181,8 +181,23 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void { } }.f); + // Separate watchlist file (backward compat). Loaded here at the run + // scope - NOT inside the collection block below - so its symbol + // strings outlive the `display` call at the end of `run`. + // `watch_list` (and `watch_prices`' keys) borrow these slices; + // freeing them before display - the previous behavior, where the + // load + `defer freeWatchlist` lived inside the collection block - + // rendered the file's watch symbols as freed-memory garbage on the + // CLI. The TUI is unaffected: it keeps watchlist symbols in its + // long-lived arena. + const wl_syms: ?[][]const u8 = if (watchlist_path) |wl_path| + cli.loadWatchlist(io, allocator, wl_path) + else + null; + defer cli.freeWatchlist(allocator, wl_syms); + // Collect watch symbols and their prices for display. - // Includes watch lots from portfolio + symbols from separate watchlist file. + // Includes watch lots from portfolio + symbols from the watchlist file. var watch_list: std.ArrayList([]const u8) = .empty; defer watch_list.deinit(allocator); var watch_prices = std.StringHashMap(f64).init(allocator); @@ -208,18 +223,14 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void { } } - // Separate watchlist file (backward compat) - if (watchlist_path) |wl_path| { - const wl_syms = cli.loadWatchlist(io, allocator, wl_path); - defer cli.freeWatchlist(allocator, wl_syms); - if (wl_syms) |syms_list| { - for (syms_list) |sym| { - if (watch_seen.contains(sym)) continue; - try watch_seen.put(sym, {}); - try watch_list.append(allocator, sym); - if (svc.getCachedLastClose(sym)) |close| { - try watch_prices.put(sym, close); - } + // Symbols from the separate watchlist file (loaded above). + if (wl_syms) |syms_list| { + for (syms_list) |sym| { + if (watch_seen.contains(sym)) continue; + try watch_seen.put(sym, {}); + try watch_list.append(allocator, sym); + if (svc.getCachedLastClose(sym)) |close| { + try watch_prices.put(sym, close); } } }