Announcing Corundum 0.3.3

I am pleased to announce Corundum, a crate that statically enforces persistent memory safety. Please watch this video for technical details on Corundum.

Corundum provides three new pointer types: Pbox, Prc, and Parc which are the persistent versions of their volatile counterparts (i.e., Box, Rc, and Arc). It also provides persistent memory cell types of PCell, PRefCell, and PMutex. We tried to make it idiomatic to the standard Rust as much as possible to diminish the learning curve.

Although it is meant to be a persistent memory library, its use cases go beyond that. I personally used it for multiple storage-based persistent data structures. How cool would it be if you have all your information in form of any data structure you like, but guaranteed to be persistent and crash resilient?

Please feel free to use Corundum and report bugs on our GitHub page.

-Enjoy

4 Likes

"without concerning much about crash consistency and data loss."
I read this as saying; expect corruption if your program crashes so don't use for anything you can't afford to lose.

Have you been using it? I would appreciate it if you can give us a feedback.

Just the glance at the docs. Tech might be interesting but have no problem at moment shouting out for such a library.

Thank you for bringing that to my attention. We will look into the docs more carefully.

Looks very useful for our next product. Don't hold your breath; we haven't shipped our first one yet.

1 Like

Is there a reason why you think PCell::get_ref, PCell::update_inplace, PCell::update_inplace_mut and the various math functions on PCell are sound? Because it doesn't look like they are.

use corundum::cell::PCell;
use corundum::prc::Prc;
use corundum::clone::PClone;
use corundum::alloc::{MemPool, heap::Heap};

fn main() {
	Heap::transaction(|j| {
		let foo = Prc::new(PCell::new(1), &j);
		let foo2 = foo.pclone(&j);
		
		foo.update_inplace_mut(&j, |foo| {
			foo2.update_inplace_mut(&j, |foo2| {
				// `foo` and `foo2` are mutable references to the same type,
				// so it's unsound for them to point to the same object
				assert_ne!(foo as *mut _, foo2 as *mut _); // oops, this fails
			});
		});
	}).unwrap();
}

Also, did you test this with miri? I tried running this snippet with miri, but it doesn't even get to the update_inplace_mut call, it fails when detecting an unaligned read in Heap::transation

error: Undefined Behavior: accessing memory with alignment 1, but alignment 8 is required
  --> G:\Programmi\Rust\.cargo\registry\src\github.com-1ecc6299db9ec823\corundum-0.3.3\src\utils.rs:80:5
   |
80 |     &mut *U { raw }.rf
   |     ^^^^^^^^^^^^^^^^^^ accessing memory with alignment 1, but alignment 8 is required
   |
   = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
   = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information

   = note: inside `corundum::utils::read::<corundum::stm::Journal<corundum::alloc::heap::Heap>>` at G:\Programmi\Rust\.cargo\registry\src\github.com-1ecc6299db9ec823\corundum-0.3.3\src\utils.rs:80:5
   = note: inside `<corundum::alloc::heap::Heap as corundum::alloc::MemPool>::atomic_new::<corundum::stm::Journal<corundum::alloc::heap::Heap>>` at G:\Programmi\Rust\.cargo\registry\src\github.com-1ecc6299db9ec823\corundum-0.3.3\src\alloc\pool.rs:816:17
   = note: inside closure at G:\Programmi\Rust\.cargo\registry\src\github.com-1ecc6299db9ec823\corundum-0.3.3\src\stm\journal.rs:603:51
   = note: inside `<corundum::alloc::heap::Heap as corundum::alloc::MemPool>::journals::<std::option::Option<(*const corundum::stm::Journal<corundum::alloc::heap::Heap>, *mut i32)>, [closure@corundum::stm::Journal<corundum::alloc::heap::Heap>::current::{closure#0}]>` at G:\Programmi\Rust\.cargo\registry\src\github.com-1ecc6299db9ec823\corundum-0.3.3\src\alloc\heap.rs:162:9
   = note: inside `corundum::stm::Journal::<corundum::alloc::heap::Heap>::current` at G:\Programmi\Rust\.cargo\registry\src\github.com-1ecc6299db9ec823\corundum-0.3.3\src\stm\journal.rs:598:13
   = note: inside closure at G:\Programmi\Rust\.cargo\registry\src\github.com-1ecc6299db9ec823\corundum-0.3.3\src\alloc\pool.rs:1137:29
   = note: inside `std::panicking::r#try::do_call::<[closure@<corundum::alloc::heap::Heap as corundum::alloc::MemPool>::transaction<(), [closure@src\main.rs:7:20: 21:3]>::{closure#0}], ()>` at G:\Programmi\Rust\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\panicking.rs:379:40
   = note: inside `std::panicking::r#try::<(), [closure@<corundum::alloc::heap::Heap as corundum::alloc::MemPool>::transaction<(), [closure@src\main.rs:7:20: 21:3]>::{closure#0}]>` at G:\Programmi\Rust\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\panicking.rs:343:19
   = note: inside `std::panic::catch_unwind::<[closure@<corundum::alloc::heap::Heap as corundum::alloc::MemPool>::transaction<(), [closure@src\main.rs:7:20: 21:3]>::{closure#0}], ()>` at G:\Programmi\Rust\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\panic.rs:431:14
   = note: inside `<corundum::alloc::heap::Heap as corundum::alloc::MemPool>::transaction::<(), [closure@src\main.rs:7:20: 21:3]>` at G:\Programmi\Rust\.cargo\registry\src\github.com-1ecc6299db9ec823\corundum-0.3.3\src\alloc\pool.rs:1108:19note: inside `main` at src\main.rs:7:2
  --> src\main.rs:7:2
   |
7  | /     Heap::transaction(|j| {
8  | |         let foo = Prc::new(PCell::new(1), &j);
9  | |         let foo2 = foo.pclone(&j);
10 | |
...  |
20 | |         });
21 | |     }).unwrap();
   | |______^
   = note: inside `<fn() as std::ops::FnOnce<()>>::call_once - shim(fn())` at G:\Programmi\Rust\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\ops\function.rs:227:5
   = note: inside `std::sys_common::backtrace::__rust_begin_short_backtrace::<fn(), ()>` at G:\Programmi\Rust\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\sys_common\backtrace.rs:125:18
   = note: inside closure at G:\Programmi\Rust\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\rt.rs:66:18
   = note: inside `std::ops::function::impls::<impl std::ops::FnOnce<()> for &dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe>::call_once` at G:\Programmi\Rust\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\ops\function.rs:259:13
   = note: inside `std::panicking::r#try::do_call::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>` at G:\Programmi\Rust\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\panicking.rs:379:40
   = note: inside `std::panicking::r#try::<i32, &dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe>` at G:\Programmi\Rust\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\panicking.rs:343:19
   = note: inside `std::panic::catch_unwind::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>` at G:\Programmi\Rust\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\panic.rs:431:14
   = note: inside `std::rt::lang_start_internal` at G:\Programmi\Rust\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\rt.rs:51:25
   = note: inside `std::rt::lang_start::<()>` at G:\Programmi\Rust\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\rt.rs:65:5

error: aborting due to previous error

Edit: a more simple example that allows to access arbitrary data

use corundum::cell::PCell;
use corundum::boxed::Pbox;
use corundum::alloc::{MemPool, heap::Heap};

fn main() {
	Heap::transaction(|j| {
		let foo = PCell::new(Ok(Pbox::new(42, &j)));
		let r = foo.get_ref().as_ref().unwrap();
		foo.set(Err((isize::MAX as usize, 0u8)), &j);
		println!("{}", *r);
	}).unwrap();
}

When run:

PS G:\WorkSpace\rust-playground> cargo +nightly run
   Compiling rust-playground v0.1.0 (G:\WorkSpace\rust-playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.37s
     Running `target\debug\rust-playground.exe`
error: process didn't exit successfully: `target\debug\rust-playground.exe` (exit code: 0xc0000005, STATUS_ACCESS_VIOLATION)
4 Likes

Looks interesting. Do you have any pointers to any materials that might help one get started setting up DAX?

Thank you for testing it out and finding these issues.

According to your example, get_ref is not sound and causes a memory problem. update_inplace_mut function is similar to the update function, so I'll remove them in the next release.

The memory management of Corundum is quite different from the standard one and the alignment is completely different. Therefore, miri may detect several alignment issues. I should meditate on this problem. Again, thank you for pointing them out.

Yes, Corundum pointers are based on DAX. Internally, it mmaps a file to the program's virtual address space and use offset + address translation + DAX to access data.

FYI in general is unsound for Cell to ever let user code get a reference to the inner data through an &Cell. update is sound because it gives a copy, but get_ref/update_inplace/ecc ecc are not because they give out references. For this same reason PCell::add ecc ecc are unsound. Also, the implementation of Clone and Debug are also unsound for the same reason. Exploiting PCell::add and its Clone/Debug implementations may be a bit more tricky because it requires calling PCell::set, which requires an &Journal and that's not PSafe, however PSafe can be tricked by using global statics.

#![feature(once_cell)]

use std::lazy::OnceCell;
thread_local! {
	static JOURNAL: OnceCell<&'static Journal<Heap>> = OnceCell::new();
}

use corundum::cell::PCell;
use corundum::boxed::Pbox;
use corundum::prc::Prc;
use corundum::alloc::{MemPool, heap::Heap};
use corundum::clone::PClone;
use corundum::stm::Journal;

enum Foo {
	None,
	Some {
		data: Result<Pbox<i32, Heap>, (usize, u8)>,
		this: Prc<PCell<Foo, Heap>, Heap>,
	}
}

impl std::ops::AddAssign for Foo {
	fn add_assign(&mut self, _: Self) {
		if let Foo::Some { data, this } = self {
			let r = data.as_ref().unwrap();
			let j = JOURNAL.with(|oc| *oc.get().unwrap());
			let new_foo = Foo::Some {
				data: Err((isize::MAX as usize, 0u8)),
				this: this.pclone(j),
			};
			this.set(new_foo, j);
			println!("{}", *r);
		}
	}
}

fn main() {
	Heap::transaction(|j| {
		JOURNAL.with(|oc| oc.set(j)).unwrap();
		let prc = Prc::new(PCell::new(Foo::None), &j);
		let foo = Foo::Some {
			data: Ok(Pbox::new(42, &j)),
			this: prc.pclone(&j),
		};
		prc.set(foo, &j);
		prc.add(Foo::None, &j);
	}).unwrap();
}

As a side note, while I was writing this I noticed that corundum::prc is the only module that has a p prefix, this seems a bit inconsistent.

Note that no memory management can change the fact that the compiler expects any reference to be properly aligned. You can make unaligned reads from raw pointers with core::ptr::read_unaligned if you want though.

1 Like

That's right. I've removed the functions working with references from PCell. The standard Cell type also has Debug and Clone functions, so I'll keep them.

Good point. I'll fix that.

The core::ptr::read_unaligned and similar pointer reader functions perform copy internally which is not what we want while dereferencing a pointer. What I intended to do is to cast a u64 or a thin pointer to a fat pointer (*const T). There might be another way to do that, but using a union was the easiest way to implement it, albeit the members have different alignments. Since the memory management is delegated to Corundum, it is actually safe. For the next major version, I will use memory alignment to make it consistent.

Note that they're implemented only for T: Copy since that allows calling debug/clone on a copy of the inner value, without creating references to it, while yours does. Here's an example of how to access arbitrary data by exploiting your Clone implementation for PCell:

#![feature(once_cell)]

use std::lazy::OnceCell;
thread_local! {
	static JOURNAL: OnceCell<&'static Journal<Heap>> = OnceCell::new();
}

use corundum::cell::PCell;
use corundum::boxed::Pbox;
use corundum::prc::Prc;
use corundum::alloc::{MemPool, heap::Heap};
use corundum::clone::PClone;
use corundum::stm::Journal;

enum Foo {
	None,
	Some {
		data: Result<Pbox<i32, Heap>, (usize, u8)>,
		this: Prc<PCell<Foo, Heap>, Heap>,
	}
}

impl Clone for Foo {
	fn clone(&self) -> Self {
		if let Foo::Some { data, this } = self {
			let r = data.as_ref().unwrap();
			let j = JOURNAL.with(|oc| *oc.get().unwrap());
			let new_foo = Foo::Some {
				data: Err((isize::MAX as usize, 0u8)),
				this: this.pclone(j),
			};
			this.set(new_foo, j);
			println!("{}", *r);
		}
		unimplemented!();
	}
}

fn main() {
	Heap::transaction(|j| {
		JOURNAL.with(|oc| oc.set(j)).unwrap();
		let prc = Prc::new(PCell::new(Foo::None), &j);
		let foo = Foo::Some {
			data: Ok(Pbox::new(42, &j)),
			this: prc.pclone(&j),
		};
		prc.set(foo, &j);
		let _ = prc.clone();
	}).unwrap();
}

Unaligned references being UB has nothing to do with the allocator, it's language UB for them to be unaligned. The compiler expects them to be aligned, and may use that fact for e.g. store informations in the unused bits.

This means you're also reading uninitialized data.

I see. I have made Clone and Debug on Copy types. I also will find a solution for the alignment. Thanks for the invaluable information :pray:t2: