From 7cb5de1c95c2d993b2bde6cf7954ed6053473027 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Tue, 3 Feb 2026 17:40:30 +0000 Subject: [PATCH 1/2] Introduce Snapshot.load This enables persisting snapshots to disk for faster startup. Complements the existing `Snapshot#dump` method. ``` snapshot = MiniRacer::Snapshot.new('var foo = "bar";') File.binwrite("snapshot.bin", snapshot.dump) blob = File.binread("snapshot.bin") restored = MiniRacer::Snapshot.load(blob) ctx = MiniRacer::Context.new(snapshot: restored) ``` --- CHANGELOG | 3 +++ README.md | 17 +++++++++++++++++ ext/mini_racer_extension/mini_racer_extension.c | 14 ++++++++++++++ test/mini_racer_test.rb | 15 +++++++++++++++ 4 files changed, 49 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 6a7ebda..7dc084a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +- unreleased + - Add Snapshot.load to restore snapshots from binary data, enabling disk persistence + - 0.19.2 - 24-12-2025 - upgrade to node 24.12.0 diff --git a/README.md b/README.md index a0e0289..5d56ff4 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,23 @@ context.eval("counter") # => 1 ``` +Snapshots can also be persisted to disk for faster startup: + +```ruby +# Save a snapshot to disk +snapshot = MiniRacer::Snapshot.new('var foo = "bar";') +File.binwrite("snapshot.bin", snapshot.dump) + +# Load it back in a later process +blob = File.binread("snapshot.bin") +snapshot = MiniRacer::Snapshot.load(blob) +context = MiniRacer::Context.new(snapshot: snapshot) +context.eval("foo") +# => "bar" +``` + +Note that snapshots are architecture and V8-version specific. A snapshot created on one platform (e.g., ARM64 macOS) cannot be loaded on a different platform (e.g., x86_64 Linux). Snapshots are best used for same-machine caching or homogeneous deployment environments. + ### Garbage collection You can make the garbage collector more aggressive by defining the context with `MiniRacer::Context.new(ensure_gc_after_idle: 1000)`. Using this will ensure V8 will run a full GC using `context.low_memory_notification` 1 second after the last eval on the context. Low memory notifications ensure long living contexts use minimal amounts of memory. diff --git a/ext/mini_racer_extension/mini_racer_extension.c b/ext/mini_racer_extension/mini_racer_extension.c index ee91f43..2b39242 100644 --- a/ext/mini_racer_extension/mini_racer_extension.c +++ b/ext/mini_racer_extension/mini_racer_extension.c @@ -1690,6 +1690,19 @@ static VALUE snapshot_dump(VALUE self) return ss->blob; } +static VALUE snapshot_load(VALUE klass, VALUE blob) +{ + Snapshot *ss; + VALUE self; + + Check_Type(blob, T_STRING); + self = snapshot_alloc(klass); + TypedData_Get_Struct(self, Snapshot, &snapshot_type, ss); + ss->blob = rb_str_dup(blob); + rb_enc_associate(ss->blob, rb_ascii8bit_encoding()); + return self; +} + static VALUE snapshot_size0(VALUE self) { Snapshot *ss; @@ -1742,6 +1755,7 @@ void Init_mini_racer_extension(void) rb_define_method(c, "warmup!", snapshot_warmup, 1); rb_define_method(c, "dump", snapshot_dump, 0); rb_define_method(c, "size", snapshot_size0, 0); + rb_define_singleton_method(c, "load", snapshot_load, 1); rb_define_alloc_func(c, snapshot_alloc); c = rb_define_class_under(m, "Platform", rb_cObject); diff --git a/test/mini_racer_test.rb b/test/mini_racer_test.rb index 0134dda..0ec523c 100644 --- a/test/mini_racer_test.rb +++ b/test/mini_racer_test.rb @@ -485,6 +485,21 @@ def test_snapshot_dump assert_equal(snapshot.size, dump.length) end + def test_snapshot_load + if RUBY_ENGINE == "truffleruby" + skip "TruffleRuby does not yet implement snapshots" + end + snapshot = MiniRacer::Snapshot.new('var foo = "bar"; function hello() { return "world"; }') + blob = snapshot.dump + + restored = MiniRacer::Snapshot.load(blob) + + assert_equal(snapshot.size, restored.size) + ctx = MiniRacer::Context.new(snapshot: restored) + assert_equal("bar", ctx.eval("foo")) + assert_equal("world", ctx.eval("hello()")) + end + def test_invalid_snapshots_throw_an_exception begin MiniRacer::Snapshot.new("var foo = bar;") From 0f2e4541351c06523344ae12d7cd122032545286 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Wed, 4 Feb 2026 10:29:45 +1100 Subject: [PATCH 2/2] fix(snapshot): ensure valid encoding on Snapshot.load Set ENC_CODERANGE_VALID on loaded snapshot blobs to prevent Ruby from treating binary data as broken encoding when the input string has non-binary encoding (e.g., UTF-8). Also adds security warning to README about loading untrusted snapshots, and expands test coverage for encoding handling. --- README.md | 2 ++ .../mini_racer_extension.c | 1 + test/mini_racer_test.rb | 21 +++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/README.md b/README.md index 5d56ff4..4e8e3b7 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,8 @@ context.eval("foo") Note that snapshots are architecture and V8-version specific. A snapshot created on one platform (e.g., ARM64 macOS) cannot be loaded on a different platform (e.g., x86_64 Linux). Snapshots are best used for same-machine caching or homogeneous deployment environments. +**Security note:** Only load snapshots from trusted sources. V8 snapshots are not designed to be safely loaded from untrusted input—malformed or malicious snapshot data may cause crashes or memory corruption. + ### Garbage collection You can make the garbage collector more aggressive by defining the context with `MiniRacer::Context.new(ensure_gc_after_idle: 1000)`. Using this will ensure V8 will run a full GC using `context.low_memory_notification` 1 second after the last eval on the context. Low memory notifications ensure long living contexts use minimal amounts of memory. diff --git a/ext/mini_racer_extension/mini_racer_extension.c b/ext/mini_racer_extension/mini_racer_extension.c index 2b39242..7cd1fa1 100644 --- a/ext/mini_racer_extension/mini_racer_extension.c +++ b/ext/mini_racer_extension/mini_racer_extension.c @@ -1700,6 +1700,7 @@ static VALUE snapshot_load(VALUE klass, VALUE blob) TypedData_Get_Struct(self, Snapshot, &snapshot_type, ss); ss->blob = rb_str_dup(blob); rb_enc_associate(ss->blob, rb_ascii8bit_encoding()); + ENC_CODERANGE_SET(ss->blob, ENC_CODERANGE_VALID); return self; } diff --git a/test/mini_racer_test.rb b/test/mini_racer_test.rb index 0ec523c..211b052 100644 --- a/test/mini_racer_test.rb +++ b/test/mini_racer_test.rb @@ -495,11 +495,32 @@ def test_snapshot_load restored = MiniRacer::Snapshot.load(blob) assert_equal(snapshot.size, restored.size) + assert_equal(Encoding::ASCII_8BIT, restored.dump.encoding) + assert(restored.dump.valid_encoding?, "restored snapshot dump should have valid encoding") ctx = MiniRacer::Context.new(snapshot: restored) assert_equal("bar", ctx.eval("foo")) assert_equal("world", ctx.eval("hello()")) end + def test_snapshot_load_with_non_binary_encoding + if RUBY_ENGINE == "truffleruby" + skip "TruffleRuby does not yet implement snapshots" + end + snapshot = MiniRacer::Snapshot.new('var foo = "bar";') + # Force non-binary encoding to exercise the coderange fix. + # Binary data interpreted as UTF-8 will have broken encoding. + blob = snapshot.dump.dup.force_encoding("UTF-8") + assert_equal(Encoding::UTF_8, blob.encoding) + assert(!blob.valid_encoding?, "test precondition: blob should have broken UTF-8 encoding") + + restored = MiniRacer::Snapshot.load(blob) + + assert_equal(Encoding::ASCII_8BIT, restored.dump.encoding) + assert(restored.dump.valid_encoding?, "restored snapshot should have valid binary encoding") + ctx = MiniRacer::Context.new(snapshot: restored) + assert_equal("bar", ctx.eval("foo")) + end + def test_invalid_snapshots_throw_an_exception begin MiniRacer::Snapshot.new("var foo = bar;")