Open novacrazy opened 4 years ago
Good find, thanks!
Hello, the problem here is that this function is incredibly unsound. A MemoryBuffer
provides "read-only access to a block of memory" (docs), which means that when one is made with create_from_memory_range
it still points into the original source &[u8]
.
This means that if and when that original &[u8]
is dropped, the underlying memory is invalidated and causes UB. This can also cause UB because the Rust owner of the byte slice can freely mutate the slice after they've created the MemoryBuffer
while LLVM could be reading it, causing more UB.
Additionally, a MemoryBuffer
must be null terminated as stated in the description of MemoryBuffer
, and inkwell
currently does not check for this.
The docs of create_from_memory_range
also say that the function is "leaking memory" when it isn't, it's just allowing LLVM to look at a slice of Rust-owned memory in a read-only fashion.
@Kixiron in the example above the &[u8] is 'static, so this does not hold any water.
MemoryBuffer
does not have any lifetime constraints and the slice it receives can have an arbitrary lifetime, which means you can trivially cause use-after-free scenarios
That is true but how is that relevant for this bug?
As an additional data point, the following snippet will work fine
let source = "target triple = \"arm64-apple-macosx14.0.0\" \
define void @some_function() #0 { ret void }\n";
let memory_buffer = MemoryBuffer::create_from_memory_range_copy(source.as_bytes(), "source");
context.create_module_from_ir(memory_buffer).map_err(|message| message.to_string()).unwrap()
whereas this modified version will panic on the unwrap
let source = "target triple = \"arm64-apple-macosx14.0.0\" \
define void @some_function() #0 { ret void }\n";
let memory_buffer = MemoryBuffer::create_from_memory_range(source.as_bytes(), "source");
context.create_module_from_ir(memory_buffer).map_err(|message| message.to_string()).unwrap()
The difference between the two code fragments is the use of MemoryBuffer::create_from_memory_range_copy()
in the first case, and MemoryBuffer::create_from_memory_range()
in the second.
Describe the Bug:
This code will result in a panic from LLVM saying
expected top-level entity
from the LLVM IR.However, if I were to mark
build_prelude
as#[inline(always)]
, suddenly it doesn't panic.JitContext
is just a small wrapper aroundinkwell::context::Context
.Solution
Use
MemoryBuffer::create_from_memory_range_copy
. Seemscreate_from_memory_range
leaking memory causes it to become corrupted or something funky, but if it's inline then the value is kept on the stack long enough for LLVM to parse it. I don't know. I've also only just discovered this, so it's possiblecreate_from_memory_range_copy
may also have issues, but so far it at least fixes that odd behavior.Details
I build debug with
opt-level=3
, so perhaps that has something to do with it.If you're curious, here is the current
prelude.llvm
. It's just some intrinsic stuff with fast call.