Skip to content

view: require an unsafe LifetimeParametric marker on OwnedView views - #381

Draft
iainmcgin wants to merge 1 commit into
mainfrom
iain/issue-376-ownedview-contract
Draft

view: require an unsafe LifetimeParametric marker on OwnedView views#381
iainmcgin wants to merge 1 commit into
mainfrom
iain/issue-376-ownedview-contract

Conversation

@iainmcgin

Copy link
Copy Markdown
Collaborator

OwnedView::<V>::decode / decode_with_options transmute the Bytes slice to &'static [u8] and hand it to V::decode_view, with V: MessageView<'static> as the only bound. MessageView is safe and unsealed, and nothing required V to be parametric in the buffer lifetime — so a hand-written impl MessageView<'static> for MyView (or its own Debug/Clone/PartialEq/Drop/Serialize impl, which OwnedView invoked on the 'static-typed view) could copy a &'static str out of the buffer into longer-lived storage and dereference it after the OwnedView dropped: a use-after-free reachable from safe code. Generated views are all parametric, so only bespoke hand-written views were exposed. Input-side sibling of GHSA-9pwq-gcrx-wghh (#154).

Fix

  • New pub unsafe trait LifetimeParametric: ViewReborrow {} in buffa::view. Its # Safety section states the contract: no borrow derived from the &'static [u8] handed to decode_view may outlive the view or be exposed beyond a borrow of it — across the MessageView impl, the view's Drop/Clone/Debug/PartialEq/Hash/Serialize impls, and a ViewReborrow impl that genuinely shortens the lifetime. Parametric impls satisfy it by construction; that is what codegen emits.
  • Every OwnedView constructor (decode, decode_with_options, from_owned, from_parts) and HasMessageView::decode_view_handle / decode_view_handle_with_options require it. bytes/into_bytes need no bound; reborrow/to_owned_message need only ViewReborrow.
  • Defence in depth: OwnedView's Debug, PartialEq, Eq, Serialize and to_owned_message go through ViewReborrow::reborrow, so the view's own impls only ever run at the real buffer lifetime. ViewReborrow::Reborrowed gains a Debug bound so OwnedView<V>: Debug holds for every V: ViewReborrow without a for<'b> clause in generic code.
  • Codegen emits ::buffa::unsafe_impl_lifetime_parametric!(FooView) for every eager view (the lazy family has no OwnedView surface). The macro rather than a literal unsafe impl keeps generated output valid under #![forbid(unsafe_code)]. Checked-in generated code in buffa-types and buffa-descriptor is regenerated.
  • A compile_fail doctest on the trait shows a non-parametric capturing view rejected at the decode call; owned_view_lifetime_parametric_contract drives decode/Debug/Clone/PartialEq/reborrow/to_owned_message/drop on a hand-written view and is added to the CI Miri step.

Breaking change

Hand-written view types used through OwnedView must implement ViewReborrow (now a supertrait, and its view must be Debug) and add unsafe impl LifetimeParametric for MyView<'static> {} after auditing against the contract. Code generated by buffa 0.9 or earlier must be regenerated. Generic code calling decode_view_handle adds M::View<'static>: LifetimeParametric at the use site (the bound cannot live on the trait — the GAT normalization error still reproduces on 1.95). examples/bsr-quickstart is pinned to the published BSR plugin and was already stale against the workspace before this change; it picks this up when that plugin is next released.

Closes #376.

Reported by HackerOne researcher waynezinn.

@github-actions

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

OwnedView::decode transmutes the Bytes slice to &'static [u8] and hands it to
V::decode_view with only V: MessageView<'static> as a bound. Nothing required
V to be parametric in the buffer lifetime, so a hand-written
impl MessageView<'static> for MyView (or its own Debug/Clone/PartialEq/Drop/
Serialize impl, which OwnedView invoked on the 'static-typed view) could copy
a &'static str out of the buffer into longer-lived storage and read it after
the OwnedView dropped.

Add `pub unsafe trait LifetimeParametric: ViewReborrow {}` whose # Safety
section states the contract, require it on every OwnedView constructor and
on HasMessageView::decode_view_handle{,_with_options}, and route OwnedView's
Debug/PartialEq/Eq/Serialize/to_owned_message through ViewReborrow::reborrow
so the view's impls only ever run at the real buffer lifetime. Split the
inherent impl so bytes/into_bytes need no bound and reborrow/to_owned_message
need only ViewReborrow; put a Debug bound on ViewReborrow::Reborrowed so
OwnedView<V>: Debug holds for every V: ViewReborrow without a for<'b> clause.

Codegen emits `::buffa::unsafe_impl_lifetime_parametric!(FooView)` for every
eager view; the macro keeps generated output valid under forbid(unsafe_code).
Regenerate the checked-in buffa-types and buffa-descriptor views, add a
compile_fail doctest for a non-parametric capturing view and a Miri-targeted
positive test wired into the CI Miri step, and update the guide and DESIGN.md.

Closes #376.
@iainmcgin
iainmcgin force-pushed the iain/issue-376-ownedview-contract branch from 7f0ef17 to b827498 Compare August 27, 2026 08:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OwnedView lends a fabricated 'static buffer to arbitrary MessageView impls with no parametricity contract

1 participant