Skip to content
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
53 changes: 53 additions & 0 deletions ext/src/ruby_api/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,57 @@ impl<'a> Memory<'a> {
self.write_fixed(offset, value.to_le_bytes())
}

/// @yard
/// Read a NUL-terminated C string starting at +offset+ as an ASCII-8BIT
/// (binary) +String+.
///
/// @def read_cstring(offset)
/// @param offset [Integer]
/// @return [String]
pub fn read_cstring(ruby: &Ruby, rb_self: Obj<Self>, offset: usize) -> Result<RString, Error> {
Comment thread
saulecabrera marked this conversation as resolved.
let context = rb_self.store.context()?;
let data = rb_self.get_wasmtime_memory().data(context);

let bytes: &[u8] = match data.get(offset..) {
Some(slice) => {
let end = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
&slice[..end]
}
None => &[],
};

Ok(ruby.str_from_slice(bytes))
}

/// @yard
/// Write +value+'s bytes followed by a NUL terminator at +offset+.
///
/// @def write_cstring(offset, value)
/// @param offset [Integer]
/// @param value [String]
/// @return [void]
pub fn write_cstring(&self, offset: usize, value: RString) -> Result<(), Error> {
let slice = unsafe { value.as_slice() };
Comment thread
saulecabrera marked this conversation as resolved.
if slice.contains(&0) {
return Err(Error::new(
Ruby::get_with(value).exception_arg_error(),
"string contains null byte",
));
}
let len = slice.len();
let mut context = self.store.context_mut()?;
let dst = self
.get_wasmtime_memory()
.data_mut(&mut context)
.get_mut(offset..)
.and_then(|s| s.get_mut(..len + 1))
Comment on lines +392 to +398

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't this validate against arbitrary null bytes in the incoming Ruby string?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@saulecabrera makes sense, validation has been added in 48f0470

.ok_or_else(|| error!("out of bounds memory access"))?;
Comment thread
saulecabrera marked this conversation as resolved.

dst[..len].copy_from_slice(slice);
dst[len] = 0;
Ok(())
}

/// @yard
/// Grows a memory by +delta+ pages.
/// Raises if the memory grows beyond its limit.
Expand Down Expand Up @@ -443,6 +494,8 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
class.define_method("size", method!(Memory::size, 0))?;
class.define_method("data_size", method!(Memory::data_size, 0))?;
class.define_method("read_unsafe_slice", method!(Memory::read_unsafe_slice, 2))?;
class.define_method("read_cstring", method!(Memory::read_cstring, 1))?;
class.define_method("write_cstring", method!(Memory::write_cstring, 2))?;

unsafe_slice::init(ruby)?;

Expand Down
43 changes: 43 additions & 0 deletions spec/unit/memory_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,49 @@ module Wasmtime
end
end

describe "#read_cstring" do
it "reads NUL-terminated bytes as a binary string" do
mem = Memory.new(store, min_size: 1)
mem.write(0, "héllo\x00trailing garbage")
str = mem.read_cstring(0)
expect(str).to eq("héllo".b)
expect(str.encoding).to eq(Encoding::BINARY)
end

it "returns an empty string at a leading NUL byte" do
mem = Memory.new(store, min_size: 1)
mem.write(0, "\x00")
expect(mem.read_cstring(0)).to eq("")
end

it "returns an empty string when offset is at/past the end of memory" do
mem = Memory.new(store, min_size: 1)
expect(mem.read_cstring(mem.data_size)).to eq("")
expect(mem.read_cstring(mem.data_size + 100)).to eq("")
end
end

describe "#write_cstring" do
it "writes the bytes plus a NUL terminator and round-trips via read_cstring" do
mem = Memory.new(store, min_size: 1)
expect(mem.write_cstring(3, "héllo")).to be_nil
expect(mem.read_cstring(3)).to eq("héllo".b)
expect(mem.read(3, "héllo".bytesize + 1)).to eq("héllo\x00".b)
end

it "raises when writing past the end of the buffer" do
mem = Memory.new(store, min_size: 1)
expect { mem.write_cstring(mem.data_size, "x") }
.to raise_error(Wasmtime::Error, "out of bounds memory access")
end

it "raises when the value contains a NUL byte" do
mem = Memory.new(store, min_size: 1)
expect { mem.write_cstring(0, "foo\x00bar") }
.to raise_error(ArgumentError, "string contains null byte")
end
end

describe "#read_i64, #write_i64" do
it "round-trips a signed 64-bit integer" do
mem = Memory.new(store, min_size: 1)
Expand Down
Loading