---
title: "Kobako: Why the Sandbox Deliberately Has No Time"
date: 2026-11-11T00:00:00+08:00
publishDate: 2026-11-11T00:00:00+08:00
lastmod: 2026-11-09T21:49:14+08:00
tags: ["LLM","AI","Experience","Ruby","WebAssembly","Gem","mruby","Rust"]
series: "kobako"
toc: true
aiTranslated: true
cover: "https://blog.aotoki.me/posts/2026/11/11/kobako-why-sandbox-has-no-time/cover_hu_a5c31ed7d797e3e9.webp"
permalink: "https://blog.aotoki.me/en/posts/2026/11/11/kobako-why-sandbox-has-no-time/"
language: "en"
---

Once we finished mechanisms like RPC, Handle, and [Block](https://blog.aotoki.me/en/posts/2026/11/04/kobako-block-through-sandbox/), we already had a pleasant mruby scripting environment. But the most critical capability of a sandbox is “security” — every arbitrary script it runs should be treated as dangerous, which means we need to defend against all kinds of scenarios.

## Side-Channel Attack

A [side-channel attack](https://zh.wikipedia.org/zh-tw/%E6%97%81%E8%B7%AF%E6%94%BB%E5%87%BB) is a well-known attack technique, and a project like Kobako, which can run full Ruby syntax, is even more exposed to it. Take the following as an example.

```ruby
sandbox.bind("UserRepository", UserRepository.new(tenant: tenant))

# ...
sandbox.eval(<<~SANDBOX)
  start = Time.now
  UserRepository.find(email: "...")
  Time.now - start
SANDBOX

# => 10ms
# => 1ms
# => 1ms
# => 12ms
```

When we query four randomly chosen emails one after another, the timings fall into two ranges, around `10ms` and around `1ms`. From this we can guess that half of the emails probably exist, because a database query that hits takes a different amount of time than one that misses, or because passing through Kobako’s binding takes longer to parse.

Whatever the reason, with enough attempts, or by trying emails already leaked on the internet, an attacker can quickly learn whether those users exist in the system and then move on to further attempts.

> This assumes the sandbox is already being abused, but one less attack surface makes the whole project safer, so we have to consider what mechanisms to provide at design time.

## Frozen Time

Kobako’s default setting is called the `hermetic` mode, and it doesn’t include the native `mruby-time` extension, so nothing related to time exists inside the sandbox.

Simply put, if a script wants to ask for the time, it can’t, because there is no `Time`. Even if it tries to measure in some other way, all the sandbox sees is “how many units have passed” rather than an exact number of seconds or milliseconds, so the chance of exploiting a side-channel attack becomes very low.

However, many scenarios still need “time” to exist. In most of them we don’t need high precision — at least nobody asks questions like “at what hour, minute, and second did this user sign up?” Filtering users usually only needs precision down to the day, so we can simply use the existing binding (Bind) capability to provide a low-precision time.

```ruby
sandbox.bind("Date::Today", -> { Date.today.to_s })
```

With this, any attempt to measure “whether a user exists” fails because the granularity of “one day” is too coarse — no amount of trying can confirm it, which avoids the problem to a certain degree.

Of course, most scenarios don’t need such an extreme design. Providing time down to the second is usually enough to make large-scale abuse and probing hard; before that happens, the attempts are likely to be blocked by sandbox limits or rate limiting anyway.

```ruby
sandbox.bind("CurrentTime", -> { Time.now.to_i })
```

A design like this is basically more than enough for most scenarios.

## Security Trade-off

This fully sealed design was basically safe in Kobako’s early use cases, but to support AI use cases we recently started supporting a [Virtual Filesystem](https://blog.aotoki.me/en/posts/2026/10/21/kobako-native-filesystem-support/) and Networking, which brings these risks back into the project.

```ruby
sandbox.bind("UserRepository", UserRepository.new(tenant: tenant))

# ...
sandbox.eval(<<~SANDBOX)
  File.write('/mnt/.timestamp', '')
  start = File.mtime('/mnt/.timestamp')
  UserRepository.find(email: "...")
  File.write('/mnt/.timestamp', '')
  File.mtime('/mnt/.timestamp') - start
SANDBOX
```

Once we can access files, a new hole opens right away: the file’s modification time is exposed, and in the end time can still be inferred.

That’s why, when designing the virtual filesystem, Kobako pins `mtime` (modification time) to the start of 1970 (the Epoch) under `hermetic` mode, which closes off this path.

Still, there are plenty of other possibilities, such as this approach:

```ruby
sandbox.eval(<<~SANDBOX)
  start = Net::HTTP.get('https://hacker.com/timestamp').to_i
  UserRepository.find(email: "...")
  Net::HTTP.get('https://hacker.com/timestamp').to_i - start
SANDBOX
```

Suppose there is a server that reliably returns the current time; the program in the sandbox only needs to send it requests. In practice this is considerably harder, though. Unless the network path happens to be very close, the response can’t come back in a short enough time, and the difference most likely can’t be measured.

Another possible path is querying the host environment, where latency usually stays within `1ms` and could be exploited. This is why most sandboxes block access to the internal network: besides preventing lateral movement (finding a weakness, then moving on to the target host), it also prevents this kind of path.

Kobako’s networking feature is still in development, and we’ve given these problems a lot of thought. Once the ease-of-use problems are solved, the biggest challenge becomes keeping the environment safe to use — balancing security, speed, and lightness will be a difficult challenge.
