Description
Context
When creating a new, empty DemiBuffer
via DemiBuffer::new
, the constructor initializes data_len
and pkt_len
to buf_len
(all fields of MetaData
). This area has a few related issues:
-
The
Deref
implementation creates a slice containing all DemiBuffer bytes in the range[0, data_len)
. Dereferencing a new DemiBuffer is undefined behavior, as it violates the safety requirements ofslice::from_raw_parts
, namely (emphasis added):data
must point tolen
consecutive properly initialized values of typeT
.and from the
GlobalAlloc::alloc
documentation:The allocated block of memory may or may not be initialized.
-
This behavior is inconsistent with DPDK. The closest equivalent function is probably
rte_pktmbuf_alloc
, which initializesbuf_len
depending on the memory pool butdata_len
andpkt_len
are initially zero. The expected workflow here is to allocate an mbuf from the pool, then use method such asrte_pktmbuf_append
as bytes in the packet are initialized. -
Network transports used by LibOSes such as Catnap accept a
DemiBuffer
in theirpop
calls (e.g., in src\rust\catnap\win\transport.rs,SharedCatnapTransport::pop
). These methods depend onDemiBuffer::len()
method to determine how much data they may fill, however, as noted above, this relies ondata_len
, which should indicate the number of initialized/used bytes. Currently, these LibOSes rely on the UB described above.
Proposed Solution
Make the following changes:
- Initialize the
data_len
andpkt_len
fields ofMetaData
to zero for new (i.e., not cloned/copied)DemiBuffers
. - Add a
buffer_len()
or equivalent method toDemiBuffer
, exposing thebuf_len
MetaData field. - Add
append
andprepend
methods for manipulating theDemiBuffer
length and content. There are many considerations here:- Ideally, these would be safe methods. The naive way to accomplish this would use copies (e.g.,
append(content: &[u8])
-> copycontent
into the buffer); however, this is not a great solution for performance-sensitive applications. - The unstable extension core_io_borrowed_buf offers an interesting idiom here. Borrowing a slice of [MaybeUninit] as a
BorrowedBuf
could offer a safe idiom as follows (not taking into account chaining, lifetimes elided):pub fn append<F: FnOnce(BorrowedBuf) -> BorrowedBuf>(&mut self, fn: F) { let buf = fn(BorrowedBuf::from(unsafe { slice::from_raw_parts_mut::<MaybeUninit<u8>>(self.data_ptr().cast().offset(self.len()), self.buffer_len()) })); let metadata: &mut MetaData = self.as_metadata(); metadata.data_len += buf.init_len(); metadata.pkt_len += buf.init_len(); }
- Expose a series of unsafe functions which allow direct manipulation of the buffer and/or length. Require the user to initialize or indicate initialization correctly. This is likely the most performant/flexible approach.
- Combination of the above.
- Ideally, these would be safe methods. The naive way to accomplish this would use copies (e.g.,
Alternative Solutions
Alternatively, DemiBuffer might just initialize all bytes upfront. This still breaks consistency with DPDK, but provides a very simple solution which doesn't break anything else. Conceptually, DemiBuffer
still lacks counterpart methods for trim
and adjust
, so this approach likely just postpones a real solution here.