Safe abstraction: Backed?

A few times now, I've wanted some specific helper that kinda needs sibling-referential borrowing. This has happened in network protocol work, where I have 'static data (typically bytes::Bytes, but it could be String or Vec<u8>), and a parsed structure that holds a bunch of references into the data. I'd then like to pass the parsed structure around, which of course is tricky since currently (AFAIK) there aren't really facilities to do that -- and I don't think sibling/self-referential lifetimes are particularly high on the priority list?

So then I've transmuted() the lifetime of the parsed data away in order to basically store the data and the parsed structure together in a struct, making the data member private and transmuting the parsed structure to have a 'static lifetime, with a way to get a reference to the parsed structure with a lifetime atttached to the wrapper. But I'd like to know if this use of unsafe is sound -- and if there is, would it make sense to package this abstraction into a crate which maybe down the line could go into std?

There's an example in the ResponseData type. I guess a safe interface for this would somewhat hinge on the splitting API that Bytes provides. A generic version could maybe be called Backed, unless someone can come up with a better name.

Have you tried https://lib.rs/rental?


I'm not sure what the omitted lifetime in this case would be:

pub fn parsed<'a>(&'a self) -> &'a Response {

but you can't return &'a Response<'static> - that's unsafe. If you make it &'a Response<'a> it should be OK.

The other thing to be careful about is dropping. If Bytes gets dropped first, then Drop of Response is going to have UAF bug. That might need ManuallyDrop to enforce the right order (or at least ensure that the default drop order drops Response before Bytes)

The automatic drops follow declaration order (RFC 1857), so you can rely on that without resorting to ManuallyDrop.

1 Like

I don't understand the problem. What's unsafe about a shorter-lived borrow to 'static data (even If that data was transmuted into a 'static lifetime)?

Is it that transmuting non-'static data to something that promises it'll exist until the end of the program can cause trouble if the data is dropped somehow anyway?
I mean I agree that in general that could be problematic, but what if the author has manually verified / ensured that that invariant holds up?

I presume that in Response<'static> that lifetime is for the data backing response's memory, e.g.

struct Response<'a> {
    body: &'a [u8],
}

so having Response<'static> you could write:

let body: &'static [u8] = response.body;
drop(response);
drop(backend_holding_the_response);
use_after_free_bug(body);

because in &'a Response the 'a lifetime refers only to the lifetime of the "wrapper" type, but not its contents. If you manage to "leak" the fake-'static contents out of the wrapper, it will be allowed to outlive 'a and the universe will explode.

1 Like

I'm aware of rental, but seems like a pretty complex solution when I only need this very simple use.

Yeah, making it &'a Response<'a> makes sense. In another crate, I've even made the ResponseData equivalent impl Deref for the Response equivalent, this seems pretty okay.

FWIW, it would still be 'a: if there is a "hole" / an elided lifetime parameter in the return type, it uses the lifetime parameter used in the input type(s), when that one is unambiguous* (but I agree that using &'a Response<'_> does not express that intent clearly enough).

* by "unambiguous" I mean when there is only one lifetime parameter used for all the input types, or just for the receiver type in the case of methods.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.