-
Notifications
You must be signed in to change notification settings - Fork 456
RFC: rust: chrdev: fix use-after-free on module unload #207
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,6 @@ | |
use alloc::boxed::Box; | ||
use core::convert::TryInto; | ||
use core::marker::PhantomPinned; | ||
use core::mem::MaybeUninit; | ||
use core::pin::Pin; | ||
|
||
use crate::bindings; | ||
|
@@ -20,10 +19,67 @@ use crate::error::{Error, Result}; | |
use crate::file_operations; | ||
use crate::types::CStr; | ||
|
||
/// Character device. | ||
/// | ||
/// # Invariants | ||
/// | ||
/// - [`self.0`] is valid and non-null. | ||
/// - [`(*self.0).ops`] is valid, non-null and has static lifetime. | ||
/// - [`(*self.0).owner`] is valid and, if non-null, has module lifetime. | ||
struct Cdev(*mut bindings::cdev); | ||
|
||
impl Cdev { | ||
fn alloc( | ||
fops: &'static bindings::file_operations, | ||
module: &'static crate::ThisModule, | ||
) -> Result<Self> { | ||
// SAFETY: FFI call. | ||
let cdev = unsafe { bindings::cdev_alloc() }; | ||
if cdev.is_null() { | ||
return Err(Error::ENOMEM); | ||
} | ||
// SAFETY: `cdev` is valid and non-null since `cdev_alloc()` | ||
// returned a valid pointer which was null-checked. | ||
unsafe { | ||
(*cdev).ops = fops; | ||
(*cdev).owner = module.0; | ||
} | ||
// INVARIANTS: | ||
// - [`self.0`] is valid and non-null. | ||
// - [`(*self.0).ops`] is valid, non-null and has static lifetime, | ||
// because it was coerced from a reference with static lifetime. | ||
// - [`(*self.0).owner`] is valid and, if non-null, has module lifetime, | ||
// guaranteed by the [`ThisModule`] invariant. | ||
Ok(Self(cdev)) | ||
} | ||
|
||
fn add(&mut self, dev: bindings::dev_t, count: c_types::c_uint) -> Result { | ||
// SAFETY: according to the type invariants: | ||
// - [`self.0`] can be safely passed to [`bindings::cdev_add`]. | ||
// - [`(*self.0).ops`] will live at least as long as [`self.0`]. | ||
// - [`(*self.0).owner`] will live at least as long as the | ||
// module, which is an implicit requirement. | ||
let rc = unsafe { bindings::cdev_add(self.0, dev, count) }; | ||
if rc != 0 { | ||
return Err(Error::from_kernel_errno(rc)); | ||
} | ||
Ok(()) | ||
} | ||
} | ||
|
||
impl Drop for Cdev { | ||
fn drop(&mut self) { | ||
// SAFETY: [`self.0`] is valid and non-null by the type invariants. | ||
unsafe { | ||
bindings::cdev_del(self.0); | ||
} | ||
} | ||
} | ||
|
||
struct RegistrationInner<const N: usize> { | ||
dev: bindings::dev_t, | ||
used: usize, | ||
cdevs: [MaybeUninit<bindings::cdev>; N], | ||
cdevs: [Option<Cdev>; N], | ||
_pin: PhantomPinned, | ||
} | ||
|
||
|
@@ -96,10 +152,11 @@ impl<const N: usize> Registration<{ N }> { | |
if res != 0 { | ||
return Err(Error::from_kernel_errno(res)); | ||
} | ||
const NONE: Option<Cdev> = None; | ||
this.inner = Some(RegistrationInner { | ||
dev, | ||
used: 0, | ||
cdevs: [MaybeUninit::<bindings::cdev>::uninit(); N], | ||
cdevs: [NONE; N], | ||
_pin: PhantomPinned, | ||
}); | ||
} | ||
|
@@ -108,22 +165,13 @@ impl<const N: usize> Registration<{ N }> { | |
if inner.used == N { | ||
return Err(Error::EINVAL); | ||
} | ||
let cdev = inner.cdevs[inner.used].as_mut_ptr(); | ||
// SAFETY: Calling unsafe functions and manipulating `MaybeUninit` | ||
// pointer. | ||
unsafe { | ||
bindings::cdev_init( | ||
cdev, | ||
// SAFETY: The adapter doesn't retrieve any state yet, so it's compatible with any | ||
// registration. | ||
file_operations::FileOperationsVtable::<Self, T>::build(), | ||
); | ||
(*cdev).owner = this.this_module.0; | ||
let rc = bindings::cdev_add(cdev, inner.dev + inner.used as bindings::dev_t, 1); | ||
if rc != 0 { | ||
return Err(Error::from_kernel_errno(rc)); | ||
} | ||
} | ||
|
||
// SAFETY: The adapter doesn't retrieve any state yet, so it's compatible with any | ||
// registration. | ||
let fops = unsafe { file_operations::FileOperationsVtable::<Self, T>::build() }; | ||
let mut cdev = Cdev::alloc(fops, &this.this_module)?; | ||
cdev.add(inner.dev + inner.used as bindings::dev_t, 1)?; | ||
inner.cdevs[inner.used].replace(cdev); | ||
inner.used += 1; | ||
Ok(()) | ||
} | ||
|
@@ -149,12 +197,14 @@ unsafe impl<const N: usize> Sync for Registration<{ N }> {} | |
impl<const N: usize> Drop for Registration<{ N }> { | ||
fn drop(&mut self) { | ||
if let Some(inner) = self.inner.as_mut() { | ||
// SAFETY: Calling unsafe functions, `0..inner.used` of | ||
// `inner.cdevs` are initialized in `Registration::register`. | ||
// Replicate kernel C behaviour: drop [`Cdev`]s before calling | ||
// [`bindings::unregister_chrdev_region`]. | ||
for i in 0..inner.used { | ||
TheSven73 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
inner.cdevs[i].take(); | ||
} | ||
// SAFETY: [`self.inner`] is Some, so [`inner.dev`] was previously | ||
// created using [`bindings::alloc_chrdev_region`]. | ||
unsafe { | ||
for i in 0..inner.used { | ||
bindings::cdev_del(inner.cdevs[i].as_mut_ptr()); | ||
} | ||
bindings::unregister_chrdev_region(inner.dev, N.try_into().unwrap()); | ||
TheSven73 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add a comment here about why the // N.try_into() returned Some in [`register`], and N is constant, so it will return
// Some here too, which ensures [`unwrap`] will not panic.
bindings::unregister_chrdev_region(inner.dev, N.try_into().unwrap()); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, please! In fact, I think we could start
At the moment, we have likely too many places due to allocations, so it can wait since otherwise we would be flooded with this comments; but later on we really should not have too many of these comments, which makes it worth the noise to have them, I think. Thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed, the whole fallible allocations thing is still in flux, so adding these annotations all over the place probably won't be productive. So let's defer until later. That said, I'm not sure, but I think we can eliminate the let mut inner = this.inner.as_mut().unwrap(); simply by folding the Not sure if it's worth doing a follow-up PR for this (if it does indeed work)? Maybe there are more important fish to fry at this point? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PS I'm not sure what the CI failure is about? The CI worked fine in my cloned repo. Same git id. https://github.com/TheSven73/linux/actions/runs/778067926 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm... I just re-triggered them.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We could still start annotating the ones unrelated to allocations, though (nevertheless, I am not asking to be added here, since we still need to discuss whether to do it or not). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Never mind, I'm running into borrow checker issue due to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
} | ||
|
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.
Does just putting
None
here not work?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.
Unfortunately not... see here