-
Notifications
You must be signed in to change notification settings - Fork 109
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
Type-inferred scope guard Allow and Subscribe API design #341
Comments
I realize the above writeup is rather rough (probably hard to read/understand) -- if I think it's worth pursuing I'll refine it further. Future improvements:
|
I like this idea and the use of ZST to guarding things. I didn't know the best way to give feedback on the code, so I made edits from your playground here. Basically my comments are:
Thanks for putting together the playground to work with! |
Yes, I intended to do both of those.
That change isn't compatible with the Tock threat model, because it allows malicious capsules to read memory that was not explicitly shared with them. In particular: a capsule can hold on to the buffer past the unallow call (returning a different buffer). The only thing
By doing that, you're making the caller specify both separately rather than inferring them as a pair. That leaks implementation details into the calling code, which I'd rather not do.
That issue is outside the scope of this PR, but I'm fairly confident that
|
Moved comment to #340 |
Are malicious capsule not allowed to have If we aren't going to implement these checks (which I don't think we should unless it is under and optional feature), then we don't need to waste the ram space to store if unallow needs to be called.
My example is inferring just as much as the previous example (e.g. there are no manual types listed on |
Consider the following capsule: struct BufferEater {
has_buffer: Cell<bool>,
buffer: Cell<ReadOnlyProcessBuffer>
}
impl SyscallDriver for BufferEater {
fn allow_readonly(&self, _: ProcessId, _: usize, buffer: ReadOnlyProcessBuffer)
-> Result<ReadOnlyProcessBuffer, (ReadOnlyProcessBuffer, ErrorCode)>
{
if self.has_buffer.get() {
// Refuse to swap process buffers, return a null buffer.
return Ok(Default::default());
}
// Store the buffer forever.
self.buffer.set(buffer);
self.has_buffer.set(true);
}
} Then fn foo() {
let buffer = b"to be deallocated";
run(|ro_allow: RoAllow<1>| {
// Share the buffer with the buffer eater.
ro_allow.allow_ro(&buffer);
});
// When `run` finishes, it calls Read-Only Allow to retrieve the buffer. The syscall succeeds, but
// does not revoke `BufferEater`'s access to `buffer`.
// After foo returns, `BufferEater` will have access to something it shouldn't.
} The above Does that make sense?
These checks should be there by default. I'm willing to add an interface that skips the checks, but it will be
By grouping the syscall handles as I had originally done, I was able to simplify the run(|(console_syscalls, timer_syscalls)| {
timer.start_timer(timer_syscalls);
console.start_write(console_syscalls, buffer); The full diff relative to your playground is:
|
The problem for allow is just that the kernel does not have allow-buffer-swapping-prevention in the way that it has callback-swapping prevention, right? But if that was implemented one day (and we have talked about wanting this for awhile) we would probably want to be able to remove this extra overhead from libtock-rs. |
Yes. |
Thank you again for such thorough examples! I do now see that we should do the check to make sure the kernel does let go. Granted the kernel could still do something to that memory even if it gives you back the expected response, so you do have to draw the trust line somewhere. I think having that check on all unallows might cost valuable flash space, especially for builds where we trust the kernel. At the risk of adding feature creep, this would probably be a really nice thing to be able to disable runtime checks for. This really would be handled better with build time checks (when the kernel does the memory allowing and unallowing) I see your new example, and that is what I came up with locally after posting my shared playground. That looks good to me. To me, it seems like it leaks/ doesn't leak the same amount of implementation details, but I am not caught up on it. Either way is fine with me. Overall this approach looks good to me. |
`syscall_scope` creates a scope in which Allow and Subscribe system calls can be soundly executed, by guaranteeing that the buffers and upcalls are revoked before their lifetime ends. The exact system calls (type and ID) are inferred from the type of the closure passed to `syscall_scope`, using the `ShareList` trait. `syscall_scope` is the `run` function described at tock#341, and `ShareList` is the `SyscallList` trait.
`syscall_scope` creates a scope in which Allow and Subscribe system calls can be soundly executed, by guaranteeing that the buffers and upcalls are revoked before their lifetime ends. The exact system calls (type and ID) are inferred from the type of the closure passed to `syscall_scope`, using the `ShareList` trait. `syscall_scope` is the `run` function described at tock#341, and `ShareList` is the `SyscallList` trait.
I'm beginning to implement this design in #342 |
This API is based on the design at tock#341.
This API is based on the design at tock#341.
This API is based on the design at tock#341.
342: Add `syscall_scope`, a scope guard for system calls. r=jrvanwhy a=jrvanwhy `syscall_scope` creates a scope in which Allow and Subscribe system calls can be soundly executed, by guaranteeing that the buffers and upcalls are revoked before their lifetime ends. The exact system calls (type and ID) are inferred from the type of the closure passed to `syscall_scope`, using the `ShareList` trait. `syscall_scope` is the `run` function described at #341, and `ShareList` is the `SyscallList` trait. Co-authored-by: Johnathan Van Why <[email protected]>
This API is based on the design at tock#341.
342: Add `syscall_scope`, a scope guard for system calls. r=jrvanwhy a=jrvanwhy `syscall_scope` creates a scope in which Allow and Subscribe system calls can be soundly executed, by guaranteeing that the buffers and upcalls are revoked before their lifetime ends. The exact system calls (type and ID) are inferred from the type of the closure passed to `syscall_scope`, using the `ShareList` trait. `syscall_scope` is the `run` function described at #341, and `ShareList` is the `SyscallList` trait. Co-authored-by: Johnathan Van Why <[email protected]>
This API is based on the design at tock#341.
This API is based on the design at tock#341.
This API is based on the design at tock#341.
This API is based on the design at tock#341.
This API is based on the design at tock#341.
This API is based on the design at tock#341.
This API is based on the design at tock#341.
This API is based on the design at tock#341.
This API is based on the design at tock#341.
This API is based on the design at tock#341.
This API is based on the design at tock#341.
This API is based on the design at tock#341.
344: Add a Subscribe API to `libtock_platform`. r=jrvanwhy a=jrvanwhy This API is based on the design at #341. Based on a request from Ti50, I added a configuration parameter to `subscribe`. Currently the only part of `subscribe` that is configurable is its behavior if the kernel returns a non-null upcall. By default I don't think `subscribe` should do anything special for that case (because it "shouldn't happen" and I want to keep code size small), but Ti50 wants to add a debug print to that branch. I also provided default `Upcall` implementations for several basic types (`Cell<Bool>`, `Cell<Option<(u32, u32, u32)>>`, and similar) which simply store their arguments and a flag indicating they were called. I expect these `Upcall` implementations to be used repeatedly across multiple codebases. Co-authored-by: Johnathan Van Why <[email protected]>
This design has been merged into |
Rust Playground link with this design.
Scope guards
One way in Rust to guarantee that cleanup is performed is to use a "scope guard". An example of a scope guard is
crossbeam::scope
. Using a scope guard looks like the following:In the example,
run
creates the scope and guarantees it is dropped correctly. The calling code does not own the scope, and therefore cannot move or forget it.In the case of
libtock-rs
, the scope variable needs to hold the data required to clean up system calls. For Subscribe, no data is required, as the core kernel guarantees the swapping behavior. However, for Allow, the scope variable needs a copy of the shared slice reference, so it can verify the driver returns the correct buffer during cleanup.Unfortunately, that means the size and type of the scope varies depending on what system calls are performed. Without using heap-based memory allocation, that requires
run
to know what system calls might be called.Per-syscall data types
Lets define some structs that know how to clean up after system calls. Note we don't define a constructor yet, because constructing these types is unsafe (the owning code must guarantee that
Drop::drop
is called before the lifetime'k
ends):Example driver
Although it isn't yet clear how we can instantiate these types, we can go ahead and implement a driver using them:
Application code
Application code would want to perform a write using something like:
run
implementationWith the above app,
run
would need to look something like:In the example application,
scope
would need to have the type(RoAllow<1>, Subscribe<1>)
. To allowrun
to construct scopes, we create a trait that is implemented for every type thatscope
could have. We'll call the traitSyscallList
because each scope is a list of syscalls:Interpreting a single system call as a list of one system call, we can implement
SyscallList
forRoAllow
,Subscribe
, and some tuple types:We can now modify
run
's definiton to infer the system call list type from the passed callback function, and instantiate it usingSyscallList::new()
:The text was updated successfully, but these errors were encountered: