Skip to content

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

Merged
iainmcgin merged 6 commits into
mainfrom
iain/issue-376-ownedview-contract
Sep 8, 2026
Merged

iainmcgin merged 6 commits into
mainfrom
iain/issue-376-ownedview-contract

Conversation

@iainmcgin

@iainmcgin iainmcgin commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

OwnedView::<V>::decode hands V::decode_view a slice forged to &'static [u8], with V: MessageView<'static> as the only bound. MessageView is safe and unsealed, so a hand-written non-parametric impl MessageView<'static> for MyView — or its own Debug/PartialEq/Serialize/Clone, which OwnedView invoked on the 'static-typed view — could copy a &'static str out of the buffer and read it after the OwnedView dropped: a use-after-free from safe code. Generated views were never exposed (every FooView<'a> is parametric in 'a), and no wire input reaches it.

Every OwnedView constructor now requires V: ViewLifetimeParametric, an unsafe marker trait with ViewReborrow as supertrait; codegen emits it for every generated view via buffa::unsafe_impl_view_lifetime_parametric!, whose expansion is accepted under a consumer's #![forbid(unsafe_code)]. OwnedView's Debug, PartialEq, Eq, Serialize and to_owned_message now call the view's impls on a reborrow()ed V::Reborrowed<'b>, so after this change only decode_view* and Clone observe the forged lifetime; the trait's # Safety section reduces a hand-written view's audit to those two.

A type-level fix (decode at a fresh lifetime, then extend V::Reborrowed<'a> to V) would need ViewReborrow itself to become unsafe plus a transmute in buffa.

Breaking, for 0.10.0. Upgrading: regenerate; generic callers of decode_view_handle add M::View<'static>: buffa::ViewLifetimeParametric; ViewReborrow::Reborrowed gains a Debug bound, so any hand-written ViewReborrow impl needs a Debug view; a hand-written view used through OwnedView implements Debug and ViewReborrow, then

// SAFETY: MyView<'a> is generic over 'a and every impl on it is parametric.
buffa::unsafe_impl_view_lifetime_parametric!(MyView);

examples/bsr-quickstart's checked-in output gets the macro line by hand; it cannot be regenerated until a 0.10 BSR plugin is published, and it already fails to build on main for unrelated staleness.

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
… per-constructor bounds

Merge main (#379 removed OwnedView's Drop impl in favour of field order +
MaybeDangling; #394 added three WKT view files) and rework on top:

- rename the marker LifetimeParametric -> ViewLifetimeParametric and the
  macro to unsafe_impl_view_lifetime_parametric!, which now accepts a path;
- move the marker bound from the constructor impl block to a where-clause
  on each constructor (decode, decode_with_options, from_owned, from_parts)
  so a missing impl reports E0277 with the on_unimplemented notes instead
  of E0599, and update the compile_fail doctest accordingly; Clone carries
  the bound too;
- # Safety docs: drop-order wording follows main, state that only
  decode_view* and Clone observe the forged 'static after the reborrow
  routing, why Drop and V::Owned cannot leak, and cite yoke::Yokeable;
- guide: hand-written views used through OwnedView also need Debug and
  ViewReborrow; macro example; correct the PartialEq claim for generated
  views;
- changelog: split into a Security entry and a migration-led Breaking
  entry;
- buffa-test: a #[forbid(unsafe_code)] module including generated views,
  and a generic HRTB Serialize caller; the PanicOnFirstDropView test from
  main gains the marker;
- CI: fold the Miri test into main's OwnedView soundness step;
- regenerate WKT and descriptor view types (incl. api/type/source_context);
  add the macro call to the checked-in bsr-quickstart output.
@iainmcgin iainmcgin changed the title view: require an unsafe LifetimeParametric marker on OwnedView views view: require an unsafe ViewLifetimeParametric marker on OwnedView views Sep 6, 2026
…o accepts ::paths

Re-review follow-ups: the Safety audit and the delegation comment now list
reborrow alongside decode_view* and Clone; the Drop claim is scoped to
lifetime-generic views; the macro takes leading-:: paths; the
forbid(unsafe_code) canary moves onto the existing inline_field module
instead of including the generated code twice.
rustc 1.75 registers the higher-ranked bound before inferring V from the
argument; reproduced locally with the MSRV lockfile. Changelog notes the
same for downstream generic callers on old toolchains.
@iainmcgin
iainmcgin force-pushed the iain/issue-376-ownedview-contract branch from e3dccab to c1d39a0 Compare September 6, 2026 16:49
@iainmcgin
iainmcgin marked this pull request as ready for review September 6, 2026 17:22
@iainmcgin
iainmcgin added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 18476fa Sep 8, 2026
11 checks passed
@iainmcgin
iainmcgin deleted the iain/issue-376-ownedview-contract branch September 8, 2026 15:28
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

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

2 participants