Skip to content

Commit 47f3218

Browse files
authored
Add read_cstring/write_cstring memory helpers (#611)
* Add read_cstring/write_cstring memory helpers * Make read_cstring return ascii bytes * write_cstring add null byte validation
1 parent 4178eb0 commit 47f3218

2 files changed

Lines changed: 96 additions & 0 deletions

File tree

ext/src/ruby_api/memory.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,57 @@ impl<'a> Memory<'a> {
352352
self.write_fixed(offset, value.to_le_bytes())
353353
}
354354

355+
/// @yard
356+
/// Read a NUL-terminated C string starting at +offset+ as an ASCII-8BIT
357+
/// (binary) +String+.
358+
///
359+
/// @def read_cstring(offset)
360+
/// @param offset [Integer]
361+
/// @return [String]
362+
pub fn read_cstring(ruby: &Ruby, rb_self: Obj<Self>, offset: usize) -> Result<RString, Error> {
363+
let context = rb_self.store.context()?;
364+
let data = rb_self.get_wasmtime_memory().data(context);
365+
366+
let bytes: &[u8] = match data.get(offset..) {
367+
Some(slice) => {
368+
let end = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
369+
&slice[..end]
370+
}
371+
None => &[],
372+
};
373+
374+
Ok(ruby.str_from_slice(bytes))
375+
}
376+
377+
/// @yard
378+
/// Write +value+'s bytes followed by a NUL terminator at +offset+.
379+
///
380+
/// @def write_cstring(offset, value)
381+
/// @param offset [Integer]
382+
/// @param value [String]
383+
/// @return [void]
384+
pub fn write_cstring(&self, offset: usize, value: RString) -> Result<(), Error> {
385+
let slice = unsafe { value.as_slice() };
386+
if slice.contains(&0) {
387+
return Err(Error::new(
388+
Ruby::get_with(value).exception_arg_error(),
389+
"string contains null byte",
390+
));
391+
}
392+
let len = slice.len();
393+
let mut context = self.store.context_mut()?;
394+
let dst = self
395+
.get_wasmtime_memory()
396+
.data_mut(&mut context)
397+
.get_mut(offset..)
398+
.and_then(|s| s.get_mut(..len + 1))
399+
.ok_or_else(|| error!("out of bounds memory access"))?;
400+
401+
dst[..len].copy_from_slice(slice);
402+
dst[len] = 0;
403+
Ok(())
404+
}
405+
355406
/// @yard
356407
/// Grows a memory by +delta+ pages.
357408
/// Raises if the memory grows beyond its limit.
@@ -443,6 +494,8 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
443494
class.define_method("size", method!(Memory::size, 0))?;
444495
class.define_method("data_size", method!(Memory::data_size, 0))?;
445496
class.define_method("read_unsafe_slice", method!(Memory::read_unsafe_slice, 2))?;
497+
class.define_method("read_cstring", method!(Memory::read_cstring, 1))?;
498+
class.define_method("write_cstring", method!(Memory::write_cstring, 2))?;
446499

447500
unsafe_slice::init(ruby)?;
448501

spec/unit/memory_spec.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,49 @@ module Wasmtime
117117
end
118118
end
119119

120+
describe "#read_cstring" do
121+
it "reads NUL-terminated bytes as a binary string" do
122+
mem = Memory.new(store, min_size: 1)
123+
mem.write(0, "héllo\x00trailing garbage")
124+
str = mem.read_cstring(0)
125+
expect(str).to eq("héllo".b)
126+
expect(str.encoding).to eq(Encoding::BINARY)
127+
end
128+
129+
it "returns an empty string at a leading NUL byte" do
130+
mem = Memory.new(store, min_size: 1)
131+
mem.write(0, "\x00")
132+
expect(mem.read_cstring(0)).to eq("")
133+
end
134+
135+
it "returns an empty string when offset is at/past the end of memory" do
136+
mem = Memory.new(store, min_size: 1)
137+
expect(mem.read_cstring(mem.data_size)).to eq("")
138+
expect(mem.read_cstring(mem.data_size + 100)).to eq("")
139+
end
140+
end
141+
142+
describe "#write_cstring" do
143+
it "writes the bytes plus a NUL terminator and round-trips via read_cstring" do
144+
mem = Memory.new(store, min_size: 1)
145+
expect(mem.write_cstring(3, "héllo")).to be_nil
146+
expect(mem.read_cstring(3)).to eq("héllo".b)
147+
expect(mem.read(3, "héllo".bytesize + 1)).to eq("héllo\x00".b)
148+
end
149+
150+
it "raises when writing past the end of the buffer" do
151+
mem = Memory.new(store, min_size: 1)
152+
expect { mem.write_cstring(mem.data_size, "x") }
153+
.to raise_error(Wasmtime::Error, "out of bounds memory access")
154+
end
155+
156+
it "raises when the value contains a NUL byte" do
157+
mem = Memory.new(store, min_size: 1)
158+
expect { mem.write_cstring(0, "foo\x00bar") }
159+
.to raise_error(ArgumentError, "string contains null byte")
160+
end
161+
end
162+
120163
describe "#read_i64, #write_i64" do
121164
it "round-trips a signed 64-bit integer" do
122165
mem = Memory.new(store, min_size: 1)

0 commit comments

Comments
 (0)