DemiBuffer use of data_len/pkt_len is inconsistent w.r.t. DPDK creates UB
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
Derefimplementation 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):datamust point tolenconsecutive properly initialized values of typeT.and from the
GlobalAlloc::allocdocumentation: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_lendepending on the memory pool butdata_lenandpkt_lenare initially zero. The expected workflow here is to allocate an mbuf from the pool, then use method such asrte_pktmbuf_appendas bytes in the packet are initialized. -
Network transports used by LibOSes such as Catnap accept a
DemiBufferin theirpopcalls (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_lenandpkt_lenfields ofMetaDatato zero for new (i.e., not cloned/copied)DemiBuffers. - Add a
buffer_len()or equivalent method toDemiBuffer, exposing thebuf_lenMetaData field. - Add
appendandprependmethods for manipulating theDemiBufferlength 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])-> copycontentinto 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 BorrowedBufcould 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.