diff --git a/.github/workflows/liquid.yml b/.github/workflows/liquid.yml index 3a3687d75..4389d2a44 100644 --- a/.github/workflows/liquid.yml +++ b/.github/workflows/liquid.yml @@ -22,6 +22,7 @@ jobs: } - { ruby: 4.0, allowed-failure: false, rubyopt: "--yjit" } - { ruby: 4.0, allowed-failure: false, rubyopt: "--zjit" } + - { ruby: truffleruby, allowed-failure: false } # Head can have failures due to being in development - { ruby: head, allowed-failure: true } diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index a91462f43..ed6141566 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -8,10 +8,19 @@ module StandardFilters MAX_I32 = (1 << 31) - 1 private_constant :MAX_I32 - MIN_I64 = -(1 << 63) - MAX_I64 = (1 << 63) - 1 - I64_RANGE = MIN_I64..MAX_I64 - private_constant :MIN_I64, :MAX_I64, :I64_RANGE + supports_64bit_indices = begin + [][1 << 33, 1 << 33] + true + rescue RangeError + false + end + + INDEX_RANGE = if supports_64bit_indices + (-(1 << 63))..((1 << 63) - 1) + else + (-(1 << 31))..((1 << 31) - 1) + end + private_constant :INDEX_RANGE HTML_ESCAPE = { '&' => '&', @@ -214,11 +223,11 @@ def slice(input, offset, length = nil) Utils.to_s(input).slice(offset, length) || '' end rescue RangeError - if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset) + if INDEX_RANGE.cover?(length) && INDEX_RANGE.cover?(offset) raise # unexpected error end - offset = offset.clamp(I64_RANGE) - length = length.clamp(I64_RANGE) + offset = offset.clamp(INDEX_RANGE) + length = length.clamp(INDEX_RANGE) retry end end diff --git a/test/integration/security_test.rb b/test/integration/security_test.rb index f84058940..3a35d47cc 100644 --- a/test/integration/security_test.rb +++ b/test/integration/security_test.rb @@ -44,32 +44,39 @@ def test_no_instance_eval_later_in_chain end def test_does_not_permanently_add_filters_to_symbol_table - current_symbols = Symbol.all_symbols - - # MRI imprecisely marks objects found on the C stack, which can result - # in uninitialized memory being marked. This can even result in the test failing - # deterministically for a given compilation of ruby. Using a separate thread will - # keep these writes of the symbol pointer on a separate stack that will be garbage - # collected after Thread#join. - Thread.new do - test = %( {{ "some_string" | a_bad_filter }} ) - Template.parse(test).render! - nil - end.join - - GC.start - - assert_equal([], Symbol.all_symbols - current_symbols) + assert_no_new_symbols do + # MRI imprecisely marks objects found on the C stack, which can result + # in uninitialized memory being marked. This can even result in the test failing + # deterministically for a given compilation of ruby. Using a separate thread will + # keep these writes of the symbol pointer on a separate stack that will be garbage + # collected after Thread#join. + Thread.new do + test = %( {{ "some_string" | a_bad_filter }} ) + Template.parse(test).render! + nil + end.join + + GC.start + end end def test_does_not_add_drop_methods_to_symbol_table - current_symbols = Symbol.all_symbols + assert_no_new_symbols do + assigns = { 'drop' => Drop.new } + assert_equal("", Template.parse("{{ drop.custom_method_1 }}", assigns).render!) + assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!) + assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!) + end + end - assigns = { 'drop' => Drop.new } - assert_equal("", Template.parse("{{ drop.custom_method_1 }}", assigns).render!) - assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!) - assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!) + def assert_no_new_symbols + # Run once to trigger any first-time initialization which might create some symbols, + # for example autoload or lazy method parsing might create symbols on first execution. + yield + # Ensure no new symbols for further runs, i.e. the code does not leak symbols + current_symbols = Symbol.all_symbols + yield assert_equal([], Symbol.all_symbols - current_symbols) end