Summary
OwnedView::<V>::decode / decode_with_options (buffa/src/view.rs) transmute the
Bytes slice to &'static [u8] and pass it to V::decode_view, where the only bound is
V: MessageView<'static>. MessageView is a safe, unsealed trait and nothing requires V
to be parametric in the buffer lifetime. OwnedView's Debug, Clone, PartialEq,
Serialize, Drop and to_owned_message impls then call V's own methods on the
un-narrowed view (still typed 'static) rather than on a reborrow()ed V::View<'_>.
Why it is unsound
A hand-written, non-parametric impl MessageView<'static> for MyView can — inside
decode_view, or inside its own Debug/PartialEq/Serialize/Drop — copy a
&'static str / &'static [u8] borrowed from the buffer into longer-lived storage
(a static, a channel, a returned value). When the OwnedView drops its Bytes, that
reference dangles: a use-after-free reachable from 100% safe downstream code (confirmed
under Miri with a Debug impl that stashes self.name).
The # Safety argument on OwnedView (Bytes is stable and immutable; view dropped before
bytes) is correct as far as it goes but silently relies on V being lifetime-parametric,
which is neither documented nor enforced.
Reachability / severity
Low. Every codegen-emitted FooView<'a> is parametric and cannot capture the buffer;
no wire input reaches this. Only bespoke hand-written view types (a supported pattern per
the guide) are exposed. This is the input-side sibling of GHSA-9pwq-gcrx-wghh /
CVE-2026-55406, whose fix (#154, removing Deref in favour of reborrow) closed the
output-side leak but left the constructor contract implicit.
Suggested direction
- Make the requirement a checked contract at the constructor: an
unsafe marker trait
(or make hand-written MessageView impls unsafe to write, or seal MessageView to
codegen output) documenting that decode_view and the view's own trait impls must not
let the buffer lifetime escape Self.
- Defence in depth: route
Debug/Clone/PartialEq/Serialize/to_owned_message
through reborrow() (bounded on V: ViewReborrow) so V's methods only ever see
&'b V::View<'b>. Drop and decode_view itself cannot be narrowed this way, which is
why the contract is the primary fix.
- A Miri regression test with a deliberately capturing hand-written view.
Credit to HackerOne researcher waynezinn for discovering and reporting this issue through
Anthropic's security bug bounty program. Filed here for public tracking as hardening work.
Summary
OwnedView::<V>::decode/decode_with_options(buffa/src/view.rs) transmute theBytesslice to&'static [u8]and pass it toV::decode_view, where the only bound isV: MessageView<'static>.MessageViewis a safe, unsealed trait and nothing requiresVto be parametric in the buffer lifetime.
OwnedView'sDebug,Clone,PartialEq,Serialize,Dropandto_owned_messageimpls then callV's own methods on theun-narrowed view (still typed
'static) rather than on areborrow()edV::View<'_>.Why it is unsound
A hand-written, non-parametric
impl MessageView<'static> for MyViewcan — insidedecode_view, or inside its ownDebug/PartialEq/Serialize/Drop— copy a&'static str/&'static [u8]borrowed from the buffer into longer-lived storage(a
static, a channel, a returned value). When theOwnedViewdrops itsBytes, thatreference dangles: a use-after-free reachable from 100% safe downstream code (confirmed
under Miri with a
Debugimpl that stashesself.name).The
# Safetyargument onOwnedView(Bytes is stable and immutable; view dropped beforebytes) is correct as far as it goes but silently relies on
Vbeing lifetime-parametric,which is neither documented nor enforced.
Reachability / severity
Low. Every codegen-emitted
FooView<'a>is parametric and cannot capture the buffer;no wire input reaches this. Only bespoke hand-written view types (a supported pattern per
the guide) are exposed. This is the input-side sibling of GHSA-9pwq-gcrx-wghh /
CVE-2026-55406, whose fix (#154, removing
Derefin favour ofreborrow) closed theoutput-side leak but left the constructor contract implicit.
Suggested direction
unsafemarker trait(or make hand-written
MessageViewimplsunsafeto write, or sealMessageViewtocodegen output) documenting that
decode_viewand the view's own trait impls must notlet the buffer lifetime escape
Self.Debug/Clone/PartialEq/Serialize/to_owned_messagethrough
reborrow()(bounded onV: ViewReborrow) soV's methods only ever see&'b V::View<'b>.Dropanddecode_viewitself cannot be narrowed this way, which iswhy the contract is the primary fix.
Credit to HackerOne researcher
waynezinnfor discovering and reporting this issue throughAnthropic's security bug bounty program. Filed here for public tracking as hardening work.