Skip to content
This repository has been archived by the owner on Mar 7, 2021. It is now read-only.

Add RCU read bindings (#143) and each_process() iterator #250

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ const INCLUDED_VARS: &[&str] = &[
"SEEK_CUR",
"SEEK_END",
"O_NONBLOCK",
"init_task",
"TASK_COMM_LEN",
];
const OPAQUE_TYPES: &[&str] = &[
// These need to be opaque because they're both packed and aligned, which rustc
Expand Down
1 change: 1 addition & 0 deletions src/bindings_helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/random.h>
#include <linux/sched/task.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/version.h>
Expand Down
23 changes: 23 additions & 0 deletions src/helpers.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#include <linux/bug.h>
#include <linux/printk.h>
#include <linux/rcupdate.h>
#include <linux/uaccess.h>
#include <linux/version.h>
#include <linux/sched/signal.h>
#include <linux/sched/task.h>


int printk_helper(const unsigned char *s, int len)
Expand All @@ -23,6 +26,26 @@ int access_ok_helper(const void __user *addr, unsigned long n)
#endif
}

void rcu_read_lock_helper(void) {
rcu_read_lock();
}

void rcu_read_unlock_helper(void) {
rcu_read_unlock();
}

struct task_struct *next_task_helper(struct task_struct *p) {
return next_task(p);
}

void task_lock_helper(struct task_struct *p) {
return task_lock(p);
}

void task_unlock_helper(struct task_struct *p) {
return task_unlock(p);
}

/* see https://github.com/rust-lang/rust-bindgen/issues/1671 */
_Static_assert(__builtin_types_compatible_p(size_t, uintptr_t),
"size_t must match uintptr_t, what architecture is this??");
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub mod filesystem;
pub mod printk;
#[cfg(kernel_4_13_0_or_greater)]
pub mod random;
pub mod rcu;
pub mod sched;
pub mod sysctl;
mod types;
pub mod user_ptr;
Expand Down
42 changes: 42 additions & 0 deletions src/rcu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! Bindings to RCU (read-copy-update), a high-performance lockless
//! synchronization system used by many kernel data structures. At the
//! moment, only calling functions that perform RCU reads is supported.

extern "C" {
fn rcu_read_lock_helper();
fn rcu_read_unlock_helper();
}

/// A guard representing an RCU read-side critical section. Its
/// constructor calls `rcu_read_lock()` and its destructor calls
/// `rcu_read_unlock()`.
///
/// Within a read-side critical section (i.e., while at least one
/// RcuReadGuard object is instantiated), objects behind RCU-protected
/// pointers are guaranteed not to change, and so reading from them
/// (after gaining a pointer with `rcu_dereference()`) is safe.
///
/// It is an error (risk of deadlock, but not memory unsafety) to block
/// or schedule while holding an RcuReadGuard. It is also an error
/// (guaranteed deadlock) to call `synchronize_rcu()` while holding an
/// RcuReadGuard. Holding multiple guards (i.e., nesting read-side
/// critical sections) is safe.
pub struct RcuReadGuard(());

#[allow(clippy::new_without_default)]
impl RcuReadGuard {
pub fn new() -> Self {
unsafe {
rcu_read_lock_helper();
}
RcuReadGuard(())
}
}

impl Drop for RcuReadGuard {
fn drop(&mut self) {
unsafe {
rcu_read_unlock_helper();
}
}
}
89 changes: 89 additions & 0 deletions src/sched.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//! APIs for interacting with the scheduler and with processes,
//! corresponding to <linux/sched.h> and related header files.
#![allow(improper_ctypes)]

use core::ptr;

use crate::bindings;
use crate::rcu;

extern "C" {
fn task_lock_helper(p: *mut bindings::task_struct);
fn task_unlock_helper(p: *mut bindings::task_struct);
fn next_task_helper(p: *mut bindings::task_struct) -> *mut bindings::task_struct;
}

/// Represents a `struct task_struct *`.
pub struct TaskStruct<'a>(&'a mut bindings::task_struct);

impl TaskStruct<'_> {
/// Returns the threadgroup ID (what userspace calls the process ID).
pub fn tgid(&self) -> i32 {
self.0.tgid
}

/// Returns the command name / process title. This is a short name,
/// typically the base name of the command, and does not have the
/// full path or arguments. It's a fixed-sized set of bytes, but by
/// convention it's interpreted as NUL-terminated.
pub fn comm(&mut self) -> [u8; bindings::TASK_COMM_LEN as usize] {
Copy link
Member

Choose a reason for hiding this comment

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

On thinking more... do we need a type that's backed by a fixed size array, but maintains a "how much of this is valid" usize and derefs to slice/mut slice?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes. smallvec is basically this except that it falls back to Vec; I think we want one that just doesn't fall back.

While we're at it I also want to bury the i8/u8 nonsense under an abstraction too (i.e. impl Deref<Target=[u8]> for something constructed from an &[i8]). Really what I think I want is OsStr from libstd :(

Copy link
Member

Choose a reason for hiding this comment

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

let mut result = [0u8; bindings::TASK_COMM_LEN as usize];
unsafe {
task_lock_helper(self.0);
}
// if only char were unsigned char
for (src, dst) in self.0.comm.iter().zip(result.iter_mut()) {
if *src == 0 {
break;
}
*dst = *src as _;
}
unsafe {
task_unlock_helper(self.0);
}
result
}
}

/// Iterate over every process on the system. Returns only processes,
/// i.e., thread group leaders.
///
/// ```
/// let g = rcu::RcuReadGuard::new();
/// for p in each_process(&g) {
/// println!("{:?}", p.comm());
/// }
geofft marked this conversation as resolved.
Show resolved Hide resolved
/// ```
struct EachProcess<'g> {
p: *mut bindings::task_struct,
_g: &'g rcu::RcuReadGuard,
}

pub fn each_process(g: &rcu::RcuReadGuard) -> impl Iterator<Item = TaskStruct> {
// unsafe is bogus here because we don't read it
// https://github.com/rust-lang/rust/issues/74843
alex marked this conversation as resolved.
Show resolved Hide resolved
EachProcess {
p: unsafe { &mut bindings::init_task },
_g: g,
}
}

impl<'g> Iterator for EachProcess<'g> {
type Item = TaskStruct<'g>;

fn next(&mut self) -> Option<TaskStruct<'g>> {
// Safety:
// - oldp is valid if not null, because it is either &init_task
// (a static location) or updated by this function.
// - next_task calls rcu_dereference internally, which is safe
// because we hold self._g.
// - The returned reference has lifetime 'g, which is valid
// because self._g lives at least that long.
Copy link

Choose a reason for hiding this comment

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

Please note that holding RCU doesn't guarantee the task list to be unchanged, RCU read-side critical section here only guarantees that the task_struct we reference here won't get freed (in side the RCU read-side critical section). We use another lock "tasklist_lock" to protect the tasklist.

let oldp = unsafe { self.p.as_mut()? };
self.p = unsafe { next_task_helper(self.p) };
if self.p == unsafe { &mut bindings::init_task } {
self.p = ptr::null_mut();
}
Some(TaskStruct(oldp))
}
}
20 changes: 20 additions & 0 deletions tests/for-each-process/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "for-each-process-tests"
version = "0.1.0"
authors = ["Alex Gaynor <[email protected]>", "Geoffrey Thomas <[email protected]>"]
edition = "2018"

[lib]
crate-type = ["staticlib"]
test = false

[features]
default = ["linux-kernel-module"]

[dependencies]
linux-kernel-module = { path = "../..", optional = true }

[dev-dependencies]
kernel-module-testlib = { path = "../../testlib" }
libc = "0.2.58"
tempfile = "3"
28 changes: 28 additions & 0 deletions tests/for-each-process/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#![no_std]

use linux_kernel_module::{self, println, rcu, sched};

struct ForEachProcessTestModule;

impl linux_kernel_module::KernelModule for ForEachProcessTestModule {
fn init() -> linux_kernel_module::KernelResult<Self> {
let g = rcu::RcuReadGuard::new();
for mut p in sched::each_process(&g) {
let comm = p.comm();
let comm_until_nul = comm.split(|c| *c == 0).next().unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

Will there be a null if the comm name uses up the full buffer?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, the buffer as returned by the kernel always includes at least one trailing NUL, but more importantly, split() always returns at least one element so we're fine either way.

println!(
"for-each-process: {:8} {}",
p.tgid(),
core::str::from_utf8(comm_until_nul).unwrap_or("[invalid UTF-8]")
);
}
Ok(ForEachProcessTestModule)
}
}

linux_kernel_module::kernel_module!(
ForEachProcessTestModule,
author: b"Fish in a Barrel Contributors",
description: b"A module for testing EachProcess",
license: b"GPL"
);
15 changes: 15 additions & 0 deletions tests/for-each-process/tests/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use std::fs::File;
use std::io::Write;

use kernel_module_testlib::{assert_dmesg_contains, with_kernel_module};

#[test]
fn test_for_each_process() {
File::create("/proc/self/comm")
.unwrap()
.write_all(b"areyouthere")
.unwrap();
with_kernel_module(|| {
assert_dmesg_contains(&[b"areyouthere"]);
geofft marked this conversation as resolved.
Show resolved Hide resolved
});
}