Skip to main content
Aotokitsuruya
Aotokitsuruya
Senior Software Developer
Published at

Kobako: Letting Blocks Pass Through the Sandbox

This article is translated by AI, if have any corrections please let me know.
This article is part of Kobako series.

The Kobako: Handle Mechanism isn’t anything special for projects that support a scripting language, but blocks are a capability unique to Ruby, and it would be a shame if we couldn’t make good use of them inside a sandbox.

But can we really write something like this?

1sandbox.bind("Seq::Map", ->(items, &blk) { items.map(&blk) })
2sandbox.eval("Seq::Map.call([1, 2, 3]) { |x| x * 2 }").value
3
4# => [2, 4, 6]

Surprisingly, a block inside the sandbox can receive calls from the outside.

Enumerator::Yielder

To make this work, we need a design that “hands the block over” for us, and CRuby happens to have an object that expresses exactly this behavior.

1y = Enumerator::Yielder.new do |x|
2  puts x
3end
4
5[1, 2, 3].each(&y)

This doesn’t look special, and a lambda seems able to do the same thing, but the most abstract part here is that the block Enumerator::Yielder.new receives belongs to whoever uses it.

Let’s look at an Enumerator example.

1e = Enumerator.new do |y|
2  pp y # => Enumerator::Yielder
3  y << 1
4  y.yield 2
5end
6
7e.each { |x| puts x }

In this case y is an Enumerator::Yielder object, and the block it receives is the one from e.each. With this property, we can turn the block inside the sandbox into a version the outside can use.

Kobako::Transport::Yielder

When mruby calls a method on an external object with a block, it first makes an outbound call and records a pointer to this internal Proc, waiting for the outside to call it.

1sandbox.eval("Sequence.map { |x| x * 2 }")

At this point, what CRuby sees is a memory address inside mruby, representing the mruby Proc, the block { |x| x * 2 }, but it can’t call it.

Kobako wraps this memory address, which CRuby can’t call, in Kobako::Transport::Yielder, turning it into the same concept as Enumerator::Yielder.

1sandbox.bind("Sequence", Enumerator.new { |y| y << 1 })
2sandbox.eval("Sequence.map { |x| x * 2 }").value # [2]

In this case, Kobako cleverly hides the calls between the Host and the Guest, leaving only CRuby’s native Yielder concept, so the block appears usable both inside and outside.

The example above fails in newer versions of Kobako. To strengthen security, only methods explicitly defined by the user are allowed by default; native #each and #map on objects like Enumerator are rejected, because several escape vulnerabilities exploited exactly this kind of boundary, so newer versions forbid them.

Language Features

Now that AI can implement features quickly and in large volume, I think mechanisms that make good use of language features are all the more valuable, because they aren’t just piling up features. They come from carefully thinking about how to design so that users get an intuitive, native experience.

This feature is useful in many places. With Rails, for example, we can easily build a design like this.

 1class MetricService
 2  def initialize(metric)
 3    @metrics = Metric.where(metric: metric)
 4  end
 5
 6  def each(&block) = @metrics.each(&block)
 7  def map(&block) = @metrics.map(&block)
 8
 9  def by_day
10    @metrics = @metrics.group(:date)
11    self
12  end
13
14  def count
15    @metrics = @metrics.count
16    self
17  end
18
19  def sum_of(type)
20    @metrics = @metrics.sum(type)
21    self
22  end
23end
24
25sandbox.bind("Metric", MetricService.new(:page_view))
26sandbox.eval("Metric.by_day.sum_of(:value).map { ... }").value

By wrapping specific objects in CRuby, mruby can process the data as if it were native. For an Agent, this is easier than reading the data out and processing it itself, because it can write a piece of code to handle the data directly, without keeping the intermediate steps in the Context and spending extra tokens on them.

This was also Kobako’s original idea: to give the Ruby ecosystem a usable Code Mode that doesn’t depend on mechanisms like container technology, but is a simple, lightweight option that’s ready to use.