Skip to content

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

Merged
merged 2 commits into from
May 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ jobs:
| sed s:$'\r'$:: \
| tee qemu-stdout.log

# The kernel should not be generating any warnings
- run: |
! grep '] WARNING:' qemu-stdout.log

# Check
- run: |
grep '] rust_minimal: Rust minimal sample (init)$' qemu-stdout.log
Expand Down
98 changes: 74 additions & 24 deletions rust/kernel/chrdev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
}

Expand Down Expand Up @@ -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],
Copy link
Member

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately not... see here

_pin: PhantomPinned,
});
}
Expand All @@ -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(())
}
Expand All @@ -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 {
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());
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a comment here about why the unwrap() can never panic? E.g.

            // 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());

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please!

In fact, I think we could start // PANIC: and // NOPANIC: annotations nearby unwrap(), expect(), etc.

  • The former would annotate those that we believe may actually panic -- so it needs to be a justification of why the outer function was not made fallible to begin with.
  • The latter would annotate those that we believe cannot panic -- so it needs to justify why that is so. i.e. this is the category that could have potentially been unwrap_unchecked().

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 unwrap here:

let mut inner = this.inner.as_mut().unwrap();

simply by folding the alloc_chrdev_region into new. In that case we wouldn't need self.inner to be an Option<RegistrationInner>, it could simply be a RegistrationInner. Things become simpler, and there's one fewer potential unwrap panic.

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... I just re-triggered them. kobject_add appears there, but looks it is not coming from us:

[    0.628134] RIP: 0010:sysfs_create_file_ns+0x73/0x90
...
[    0.628165]  kobject_add_internal+0x21a/0x3c0
[    0.628167]  kobject_add+0x88/0xe0
[    0.628170]  add_sysfs_fw_map_entry+0x61/0x70
[    0.628173]  firmware_memmap_init+0x1f/0x2a
[    0.628175]  ? firmware_map_add_early+0x57/0x57
[    0.628178]  do_one_initcall+0xb3/0x240

Copy link
Member

Choose a reason for hiding this comment

The 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.

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).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Never mind, I'm running into borrow checker issue due to the Pin, a concept which I don't understand well enough yet. I'm going to leave this well alone :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pin is indeed a bit tricky due to limitations of the language; the concept itself is easy. Hopefully the language will improve the ergonomics. Also the docs around it could be improved a bit.

}
}
Expand Down