Skip to content
Open
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
18 changes: 14 additions & 4 deletions src/cxx.cc
Original file line number Diff line number Diff line change
Expand Up @@ -448,9 +448,14 @@ static_assert(!std::is_same<Vec<std::uint8_t>::const_iterator,
Vec<std::uint8_t>::iterator>::value,
"Vec<T>::const_iterator != Vec<T>::iterator");

static const char *errorCopy(const char *ptr, std::size_t len) {
char *copy = new char[len];
std::memcpy(copy, ptr, len);
// Copy the error message into a private buffer and return it.
// If the allocation fails, nullptr is returned.
static const char *errorCopy(const char *ptr, std::size_t len) noexcept {
char *copy = new (std::nothrow) char[len + 1];
Comment on lines +451 to +454
Copy link
Owner

Choose a reason for hiding this comment

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

I am on board with handling OOM in this function without crashing the process. However I think the implementation given in this PR is not correct.

This change makes errorCopy return nullptr if it fails to allocate memory to copy the error message. That means cxxbridge1$error will return nullptr to Rust.

cxx/src/cxx.cc

Lines 458 to 460 in 03b0c8d

const char *cxxbridge1$error(const char *ptr, std::size_t len) noexcept {
return errorCopy(ptr, len);
}

That means this Rust code will be holding a NonNull<u8> which is actually null, which is immediate language-level UB.

cxx/src/result.rs

Lines 45 to 50 in 03b0c8d

extern "C" {
#[link_name = "cxxbridge1$error"]
fn error(ptr: *const u8, len: usize) -> NonNull<u8>;
}
let copy = unsafe { error(ptr, len) };

Since this is UB we cannot say what really happens, but naively it's likely the null ptr ends up as the err field of union Result, which makes the union look like it contains an ok not err.

cxx/src/result.rs

Lines 51 to 52 in 03b0c8d

let err = PtrLen { ptr: copy, len };
Result { err }

cxx/src/result.rs

Lines 13 to 24 in 03b0c8d

#[repr(C)]
#[derive(Copy, Clone)]
pub struct PtrLen {
pub ptr: NonNull<u8>,
pub len: usize,
}
#[repr(C)]
pub union Result {
err: PtrLen,
ok: *const u8, // null
}

C++ will use the nullness of that pointer to determine whether an error has occurred in the Rust function, so it will appear that it has not.

cxx/gen/src/write.rs

Lines 1126 to 1128 in 03b0c8d

writeln!(out, " if (error$.ptr) {{");
writeln!(out, " throw ::rust::impl<::rust::Error>::error(error$);");
writeln!(out, " }}");

The C++ side will continue as if a successful result has been written into the function's output location, which it has not. In reality the output location will be uninitialized. C++ will move or copy out of it which will be UB, and anything the caller does with it will be further UB, including heap corruption or other such issues depending on what data structure the return type is.

if (copy) {
std::memcpy(copy, ptr, len);
copy[len] = 0;
}
return copy;
}

Expand Down Expand Up @@ -496,7 +501,12 @@ Error &Error::operator=(Error &&other) &noexcept {
return *this;
}

const char *Error::what() const noexcept { return this->msg; }
const char *Error::what() const noexcept {
if (this->msg)
return this->msg;
else
return "<message not allocated>";
}

namespace {
template <typename T>
Expand Down