never shell-release a borrowed operand in !, catch, and result unwrap_or#593
Merged
Conversation
…nwrap_or` `!`, `catch`, and a result's `unwrap_or` released the operand's tuple shell unconditionally after pulling the ok payload out. that is correct when the operand is a fresh owned temp (`make()!` — move the payload out, free the shell), but a use-after-free when the operand is borrowed: a `T!` parameter, or a named result local. the operand's owner — the caller of the function, or the local's own scope cleanup — then frees the same three-slot block again. the struct freelist recycles the block in place, so the program prints the right answer while reading and freeing memory it does not own; only `PITH_STRUCT_FREELIST=0 valgrind` sees it. a small classifier, ir_extraction_operand_class, splits the operand the way ir_optional_subject_is_owned already splits an optional subject: a call, method-call, or await result is owned; an ident, field, or index is borrowed. for a borrowed operand the extraction now keeps its hands off the shell — the owner still holds it and the one payload count it carries — and retains the escaping heap payload so the consumer has its own count, exactly as a `.ok` read does. `!`'s error path propagates a fresh error tuple rather than handing the caller's borrowed tuple upward. owned operands are untouched. this turns the borrowed-operand use-after-free into, at worst, a bounded leak (a borrowed local whose only use is an extraction is not yet cascade-eligible, so its payload count is dropped once the caller-side cascade learns to cover call arguments — a separate follow-up). safety first: a leak never corrupts. the optional `unwrap_or` path already made this split and is left alone. it still moves a borrowed subject's inner out without a retain, which is correct and leak-free for the common single extraction — the shape std leans on (`opt.unwrap_or(bytes.empty())`). extracting the same optional local twice is a distinct, rarer pre-existing bug and is not addressed here; covering it with a blanket retain regressed that common single case into a leak, so it is left for a narrower fix. tests/cases/test_borrowed_operand_extraction drives `!`/`catch`/`unwrap_or` over borrowed parameters and named locals, ok and error paths, with owned inline operands as the control, and is valgrind-gated in MEMCHECK_CASES.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
summary
!,catch, and a result'sunwrap_orreleased the operand's tuple shellunconditionally after pulling the ok payload out. correct for a fresh owned
temp (
make()!— move the payload, free the shell), a use-after-free for aborrowed operand: a
T!parameter, or a named result local. the owner — thefunction's caller, or the local's own scope cleanup — then frees the same
three-slot block again. the struct freelist recycles the block in place, so
the program prints the right answer while reading and freeing memory it does
not own; only
PITH_STRUCT_FREELIST=0 valgrindsees it.a small classifier,
ir_extraction_operand_class, splits the operand the wayir_optional_subject_is_ownedalready splits an optional subject: call /method-call / await result is owned, an ident / field / index is borrowed. a
borrowed operand keeps its shell (the owner holds it and its one payload
count) and the extraction retains the escaping payload so the consumer has its
own count, exactly as a
.okread does.!'s error path propagates a fresherror tuple rather than the caller's borrowed one. owned operands are
untouched.
this converts the use-after-free into, at worst, a bounded leak — a borrowed
local whose only use is an extraction isn't cascade-eligible yet, so its
payload count drops once the caller-side cascade learns to cover call
arguments (a separate follow-up, now unblocked). a leak never corrupts.
scope
latent: no std function takes a
T!/T?parameter, so nothing shippedtriggered this — it is reachable only by user code that unwraps a result
parameter or a named result local.
the optional
unwrap_orpath already made this split and is left alone: itstill moves a borrowed subject's inner out without a retain, which is correct
and leak-free for the common single extraction that std leans on
(
opt.unwrap_or(bytes.empty())). extracting the same optional local twice isa distinct, rarer pre-existing bug; covering it with a blanket retain
regressed that common single case into a leak, so it's left for a narrower
fix.
measured (
PITH_STRUCT_FREELIST=0 valgrind --error-exitcode=99)!on aT!paramcatchon aT!paramunwrap_oron aT!paramopt.unwrap_or(...)on a named optional local (std's shape)make()!/catch/unwrap_orcontrolwhat was tested
make bootstrap-verifygreen,make green-tests,make memcheckclean undervalgrind including the new case,
cargo test --workspace(102). the grpc serveand connection-reuse tests pass under both backends.
tests/cases/test_borrowed_operand_extractiondrives!/catch/unwrap_orover borrowed parameters and named locals on the ok and error paths with owned
inline operands as the control, and is in MEMCHECK_CASES.