Skip to content

Fix large for-loop collection#1071

Open
stayhero wants to merge 4 commits into
Shopify:mainfrom
stayhero:fix_large_forloop_collection
Open

Fix large for-loop collection#1071
stayhero wants to merge 4 commits into
Shopify:mainfrom
stayhero:fix_large_forloop_collection

Conversation

@stayhero
Copy link
Copy Markdown

When a large Range is given in a for-loop (e.g. 1..100000000000000), it may crash the Ruby process or at least eats a lot of memory and takes a significant amount of time to render the template.

I added the following testcase to template_test.rb:

    t = Template.parse("{% for a in (1..100000000000000) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}")
    t.resource_limits.render_score_limit = 50
    assert_equal "Liquid error: Memory limits exceeded", t.render
    assert t.resource_limits.reached?

The render_score_limit does not help here because it's more of a Ruby performance problem in for.rb, where the collection = collection.to_a if collection.is_a?(Range) happens.

A fix may be to check if the given Range is larger than the given render_score_limit. I'm actually not sure if there may be a better fix (e.g. checking the resource limits somewhere else) but at least it works for the given test case.

A large collection in a for loop results in a crash or large memory
usage during render.
If a large collection is given to a for block, this may crash the render
process of the template. Setting of resource_limits does not help
because these are tested at a later stage.

This can be prevented by adding a simple check to the Liquid::For block
which makes sure that a collection size does never exceed the set
render_score_limit.
@ghost ghost added the cla-needed label Mar 11, 2019
@stayhero
Copy link
Copy Markdown
Author

stayhero commented Mar 12, 2019

BTW: You can checkout this branch to show the behavior for a large given collection without the fix:

https://github.com/stayhero/liquid/tree/large_forloop_failing_test?files=1

Copy link
Copy Markdown
Contributor

@pushrax pushrax left a comment

Choose a reason for hiding this comment

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

Great catch!

Long-term I think it would make sense to iterate ranges more efficiently without constructing the intermediate array. Some range math can be done for the common cases to handle reverse and offsets. This patch makes sense as a solution for now though.

Resource limit gets triggered without a nested loop in the test for a
large collection range.
@ghost ghost removed the cla-needed label Mar 14, 2019
@stayhero
Copy link
Copy Markdown
Author

stayhero commented Apr 3, 2019

@pushrax Anything missing to merge the PR?

@fw42
Copy link
Copy Markdown
Contributor

fw42 commented Apr 26, 2019

@pushrax: ping

@gjtorikian
Copy link
Copy Markdown
Contributor

👋 Friendly bump to see what it would take to merge.

Comment thread lib/liquid/tags/for.rb
score_limit = context.resource_limits.render_score_limit
if score_limit && score_limit < collection.size
context.resource_limits.render_score += collection.size
raise MemoryError.new("Memory limits exceeded".freeze)
Copy link
Copy Markdown
Contributor

@ianks ianks Nov 22, 2024

Choose a reason for hiding this comment

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

The general idea here is correct, but this will cause a subtle behavior difference. The semantics are that rendering should continue until the render score is reached - meaning the first 0..RENDER_SCORE items should be rendered before the memory error.

Instead, we should do something like:

if score_limit && score_limit < collection.size
  collection = collection.begin..score_limit
end

As for the For tag, we definitely should look into removing the collection = collection.to_a if collection.is_a?(Range), that's definitely a bug.

Comment thread lib/liquid/tags/for.rb
collection = collection.to_a
end

limit = context.evaluate(@limit)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We would also need to make sure we honor the limit and offsets

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants