From 251f55717051b7b696710a62570a91b14639a1a5 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sun, 7 Jun 2026 15:42:52 +0300 Subject: [PATCH 1/3] Add read_cstring/write_cstring memory helpers --- ext/src/ruby_api/memory.rs | 45 ++++++++++++++++++++++++++++++++++++++ spec/unit/memory_spec.rb | 37 +++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/ext/src/ruby_api/memory.rs b/ext/src/ruby_api/memory.rs index 14dd4103..63a76fd2 100644 --- a/ext/src/ruby_api/memory.rs +++ b/ext/src/ruby_api/memory.rs @@ -352,6 +352,49 @@ impl<'a> Memory<'a> { self.write_fixed(offset, value.to_le_bytes()) } + /// @yard + /// Read a NUL-terminated C string starting at +offset+ as a UTF-8 +String+. + /// + /// @def read_cstring(offset) + /// @param offset [Integer] + /// @return [String] + pub fn read_cstring(ruby: &Ruby, rb_self: Obj, offset: usize) -> Result { + let context = rb_self.store.context()?; + let data = rb_self.get_wasmtime_memory().data(context); + let utf8 = ruby.utf8_encoding(); + + let slice = match data.get(offset..) { + Some(slice) => slice, + None => return Ok(ruby.enc_str_new("", utf8)), + }; + let end = slice.iter().position(|&b| b == 0).unwrap_or(slice.len()); + + Ok(ruby.enc_str_new(&slice[..end], utf8)) + } + + /// @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() }; + 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)) + .ok_or_else(|| error!("out of bounds memory access"))?; + + 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. @@ -443,6 +486,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)?; diff --git a/spec/unit/memory_spec.rb b/spec/unit/memory_spec.rb index bffde274..0afaa1ed 100644 --- a/spec/unit/memory_spec.rb +++ b/spec/unit/memory_spec.rb @@ -117,6 +117,43 @@ module Wasmtime end end + describe "#read_cstring" do + it "reads a NUL-terminated string as UTF-8" 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") + expect(str.encoding).to eq(Encoding::UTF_8) + 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") + 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 + end + describe "#read_i64, #write_i64" do it "round-trips a signed 64-bit integer" do mem = Memory.new(store, min_size: 1) From 2c3f7dfbaafb6261110f9e1576447b94a5a2a4bd Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sun, 7 Jun 2026 17:28:52 +0300 Subject: [PATCH 2/3] Make read_cstring return ascii bytes --- ext/src/ruby_api/memory.rs | 16 +++++++++------- spec/unit/memory_spec.rb | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/ext/src/ruby_api/memory.rs b/ext/src/ruby_api/memory.rs index 63a76fd2..e9a1c213 100644 --- a/ext/src/ruby_api/memory.rs +++ b/ext/src/ruby_api/memory.rs @@ -353,7 +353,8 @@ impl<'a> Memory<'a> { } /// @yard - /// Read a NUL-terminated C string starting at +offset+ as a UTF-8 +String+. + /// Read a NUL-terminated C string starting at +offset+ as an ASCII-8BIT + /// (binary) +String+. /// /// @def read_cstring(offset) /// @param offset [Integer] @@ -361,15 +362,16 @@ impl<'a> Memory<'a> { pub fn read_cstring(ruby: &Ruby, rb_self: Obj, offset: usize) -> Result { let context = rb_self.store.context()?; let data = rb_self.get_wasmtime_memory().data(context); - let utf8 = ruby.utf8_encoding(); - let slice = match data.get(offset..) { - Some(slice) => slice, - None => return Ok(ruby.enc_str_new("", utf8)), + let bytes: &[u8] = match data.get(offset..) { + Some(slice) => { + let end = slice.iter().position(|&b| b == 0).unwrap_or(slice.len()); + &slice[..end] + } + None => &[], }; - let end = slice.iter().position(|&b| b == 0).unwrap_or(slice.len()); - Ok(ruby.enc_str_new(&slice[..end], utf8)) + Ok(ruby.str_from_slice(bytes)) } /// @yard diff --git a/spec/unit/memory_spec.rb b/spec/unit/memory_spec.rb index 0afaa1ed..495320e3 100644 --- a/spec/unit/memory_spec.rb +++ b/spec/unit/memory_spec.rb @@ -118,12 +118,12 @@ module Wasmtime end describe "#read_cstring" do - it "reads a NUL-terminated string as UTF-8" 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") - expect(str.encoding).to eq(Encoding::UTF_8) + 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 @@ -143,7 +143,7 @@ module Wasmtime 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") + 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 From 48f04704d472bd23840e354aa171248fa1bc6c50 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 9 Jun 2026 09:40:59 +0300 Subject: [PATCH 3/3] write_cstring add null byte validation --- ext/src/ruby_api/memory.rs | 6 ++++++ spec/unit/memory_spec.rb | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/ext/src/ruby_api/memory.rs b/ext/src/ruby_api/memory.rs index e9a1c213..f7417e2a 100644 --- a/ext/src/ruby_api/memory.rs +++ b/ext/src/ruby_api/memory.rs @@ -383,6 +383,12 @@ impl<'a> Memory<'a> { /// @return [void] pub fn write_cstring(&self, offset: usize, value: RString) -> Result<(), Error> { let slice = unsafe { value.as_slice() }; + 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 diff --git a/spec/unit/memory_spec.rb b/spec/unit/memory_spec.rb index 495320e3..9e1aba40 100644 --- a/spec/unit/memory_spec.rb +++ b/spec/unit/memory_spec.rb @@ -152,6 +152,12 @@ module Wasmtime 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