-
Notifications
You must be signed in to change notification settings - Fork 747
Rework of Digest HIL #3041
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rework of Digest HIL #3041
Conversation
DigestMut. LeasableBuffer is now immutable, while LeasableMutableBuffer is mutable.
There was a lot of discussion on the core call today; there's general agreement that having two traits is undesirable, but there is not yet a better solution. So we agreed to discuss possible better approaches here on this PR before moving forward. |
I spent some time today thinking about a different design for asynchronously passing buffers to hardware that I think resolves some of the issues here. This design is partially inspired by the DMA design described here: https://docs.rust-embedded.org/embedonomicon/dma.html , but ours has the added challenge that we cannot rely on an assumption of futures / generators to simplify waiting on results, and instead have to support an event-driven state machine. Step 1: Peripheral functions that currently take //! kernel/src/hil/uart.rs
trait Transmit {
fn transmit_buffer<B: AsRef<[u8]> + 'static>(
&self,
tx_buffer: B, // used to be &'static mut [u8]
tx_len: usize,
) -> Result<BufferHandle<B>, (ErrorCode, B)>; // Notice different return type!
} This is supported by a new type, pub trait BufferHandleTrait {
fn transfer_done(&self);
}
pub struct BufferHandle<B: AsRef<[u8]> + 'static> {
buffer: B,
finished: Cell<bool>,
}
impl<B: AsRef<[u8]> + 'static> BufferHandle {
fn get_buffer(&self) -> Option<B> {
if self.finished.get() {
Some(B)
} else {
None
}
}
}
impl<B: AsRef<[u8]> + 'static> BufferHandleTrait for BufferHandle<B> {
fn transfer_done(&self) {
self.finished.set(true);
}
} The underlying code that actually passes the buffer to hardware (e.g. for a DMA operation) passes the address / length to the underlying hardware, then stashes the buffer (whatever its concrete type) in the buffer handle. The buffer handle ensures that the actual (Rust) buffer is not made available until its callback is called indicating the underlying hardware no longer will be accessing the buffer. //! dma.rs
// sam4l transmit_buffer calls dma.prepare_transfer() to actually send the buffer, so this generic component is just forwarded
pub fn prepare_transfer<B: AsRef<[u8]> + 'static>(
&self,
pid: DMAPeripheral,
buf: B,
mut len: usize,
) -> DMAHandle<B> {
let maxlen = buf.as_ref().len()
/ match self.width.get() {
DMAWidth::Width8Bit /* DMA is acting on bytes */ => 1,
DMAWidth::Width16Bit /* DMA is acting on halfwords */ => 2,
DMAWidth::Width32Bit /* DMA is acting on words */ => 4,
};
len = cmp::min(len, maxlen);
self.registers
.mr
.write(Mode::SIZE.val(self.width.get() as u32));
self.registers.psr.set(pid);
self.registers
.marr
.write(MemoryAddressReload::MARV.val(buf.as_ref()[0] as *const u8 as u32));
self.registers
.tcrr
.write(TransferCounter::TCV.val(len as u32));
self.registers.ier.write(Interrupt::TRC::SET);
BufferHandle { buf, finished: Cell::new(false) }
} Now, we need a way to indicate to the BufferHandle that it can unlock once the transfer is done. Let's do this using the new //! dma.rs
struct DMAChannel {
// ... stuff
// we used to store buf: &'static mut [u8] here, no more
client: &'static dyn DMAClient, // same as before
handle: &'static dyn BufferHandleTrait,
}
impl DMAChannel {
pub fn handle_interrupt(&self) {
self.registers
.idr
.write(Interrupt::TERR::SET + Interrupt::TRC::SET + Interrupt::RCZ::SET);
let channel = self.registers.psr.get();
self.client.map(|client| {
client.transfer_done(channel); // normal client notification mechanism
});
self.handle.map(|handle| {
handle.transfer_done(); // indicate to handle buffer can be safely retrieved
});
}
} Finally, the capsule-level code that is calling the HIL is responsible for holding onto the handle, and retrieving its original buffer once the operation is complete: //! console.rs (I haven't thought about virtualization yet, lets assume direct access)
struct Console<U:<
8000
/span> Uart + 'static> {
uart: &'static U,
rw_buffer: TakeCell<&'static mut [u8]>, // for printing from ram buf
ro_buffer: TakeCell<&'static [u8]>, //for printing from flash buf
rw_handle: OptionalCell<BufferHandle<&'static mut [u8]>>,
ro_handle: OptionalCell<BufferHandle<&'static [u8]>>,
}
fn command(cmd_num: u32) {
match cmd_num {
0 /* print from RAM buffer */ => {
let rw_handle = self.uart.transmit_buffer(self.static_rw_buf.take());
self.rw_handle.set(Some(rw_handle)); // store handle so we can retrieve buffer upon tx_complete
}
1 /* print from flash buffer */ => {
let ro_handle = self.uart.transmit_buffer(self.static_ro_buf.take());
self.ro_handle.set(Some(ro_handle)); // store handle so we can retrieve buffer upon tx_complete
}
}
}
impl UartClient for Console {
transmit_complete() {
if self.rw_handle.get().is_some() {
self.rw_buffer.set(self.rw_handle.get().unwrap().get_buffer().unwrap());
} else if self.ro_handle.get().is_some() {
self.ro_buffer.set(self.ro_handle.get().unwrap().get_buffer().unwrap());
}
}
} The beauty here is that only the top-level of code has to think about the fact that there will be multiple different types of buffers passed to the underlying peripheral, the underlying peripheral code just cares that whatever type it was passed can be used as a static read-only slice. Also, now the upper-layer code is responsible for holding onto buffers until DMA completes, but the interface of BufferHandle prevents any unsoundness resulting from the upper-layer code accessing the buffer before the dma completes, because the buffer is not actually made accessible until the DMA operation is finished. Thoughts welcome! |
@hudson-ayers I think this sounds like a great design and might have the ability to solve many of the issues we face across these interfaces. Here's a few thoughts (I have not run this through an actual compiler, so might be wrong):
Given the above thoughts (especially 1 + 2), we might actually get away without using monomorphism in the driver methods at all. A driver might really only need to have acess to a Generally, I am really excited to see this! I will try to build a PoC of this slightly modified version, happy to collaborate on this @hudson-ayers! |
I have not fully tested an implementation of this in Tock yet, just played around with some toy code in the playground. I think I understand that the issue here is that the BufferHandle we are creating on the stack would not have a static lifetime, so the underlying dma code cannot store a static reference to it -- that is definitely a problem. I am not sure I understand your proposed solution though? Would an empty (static)
Yeah I agree there should be a capability for calls to
Yes, this was just designed to address read-only DMA operations. For DMA operations that write into the passed buffer, there is already an implicit requirement that the DMA be operating on
I am not sure I understand the suggestion here, where in my design did I suggest monomorphization in the userspace driver code?
I would love to see a PoC of this! I am definitely happy to give feedback on what you come up with |
@hudson-ayers and I just talked about this. Indeed it appears to be an issue if we were to allocate the
I was talking about monomorphization in the kernel's peripheral drivers. While the above restriction does make it possible for these drivers to accept a |
Do I have this right: Create a new type ? |
Rather that the buffer cannot be swapped or mutated while the hardware is accessing it, and the peripheral is only handed a trait object reference to it, in order to abstract over multiple buffer types. |
What prevents |
One other point I brought up with Hudson is that this really is a narrow case where we need 2 variants of a trait. My personal take is that generalization for a bounded 2 cases isn't necessary. If we had >2 cases, or a not precisely bounded set, then we want to generalize (e.g., as with |
|
The place where we ran into the mut/imut problem before was RSA keys: The resolution there was to have two traits: it doesn't generalize in the same way as |
We've re-invented mutexes! Transitioning from compile-time to run-time checks is definitely a drawback. It might be worth the code de-duplication, though. That being said, I don't think we want to use |
After going down the two traits path for a while, I decided it isn't workable. With RSA it was OK because the traits were pretty narrow. For digests, however, there are many variants; we have capsules like Unlike with RSA keys, it seems restrictive to require a client to choose mutable or immutable data. It's completely reasonable to compute a single hash over data in flash and data in RAM. The current implementation therefore puts mutable and immutable in a single trait. There's an This implementation feels pretty clean to me; the only place where the logic required a refactoring was in the |
< 6D40 div class="d-flex flex-items-center"> boards/opentitan/src/tests/hmac.rs Show resolved Hide resolved
For what its worth, the mutable/immutable buffer passing approach has been a topic of the last core team call. In essence, Hudson, Alyssa and I agree that this is a good solution to solve the particular issue of passing mutable/immutable buffers down the Digest HIL, and unblocks the signature verification work, while we should continue to explore more integrated buffer passing APIs in the near future: tock/doc/wg/core/notes/core-notes-2022-06-10.md Lines 120 to 158 in d2cf7e9
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did a careful pass through the TRD and skimmed over the rest, looks good generally, thanks @phil-levis! Just some minor notes on the TRD and the comment regarding LeasableBuffer
.
Sounds good to me. I do think the mutable/immutable tension has pretty limited scope, basically for digests and public keys. But the DMA issue is a deeper and separate one. Having a single simple approach that solves both is of course better than two different ones. The Digest HIL in this PR is definitely intended to be in-flux, and I'm open to improvements. |
e32d9b0
I've tested the OpenTitan HMAC peripheral, it now passes tests. @alistair23 |
@alistair23 Can you re-review and clear your changes requested? |
Tested on OpenTitan using Verilator; passes tests.
bors r+ |
Pull Request Overview
This pull request reworks the Digest HIL, introducing four major changes:
Digest
now has two methods to add data to a digest,add_data
andadd_mut_data
. The callback client correspondingly has two callbacksadd_data_done
andadd_mut_data_done
.LeasableBuffer
to take a&[u8]
and adds aLeasableMutableBuffer
which takes a&mut [u8]
.add_data
andadd_mut_data
is expected to add all of the data that is in the active slice of the correspondingLeasableBufffer
orLeasableMutableBuffer
. Accepting less is an error condition. Correspondingly, a successful call to one of these methods now returnsOk(())
: the prior version returnedOk(usize)
to return how many bytes would be added.Digest
to use onlyLeasableBuffer
andLeasableMutableBuffer
rather than using both these types as well as slices. This allows an error condition to indicate how much data is remaining to be added.Testing Strategy
This pull request has not been tested yet.
TODO or Help Wanted
This needs to be tested on OpenTitan.
Documentation Updated
/docs
, or no updates are required.Formatting
make prepush
.