We recently ran our Hack Monty event where we invited people to try and break out of our Monty Python sandbox.
The event was a great success:
- we had almost 1.5 million POST requests to the Hack Monty API,
- from ~650 unique IP addresses,
- ultimately leading to 65 submissions to the bounty form.
(A reminder you can browse all requests to the Hack Monty API on the public Logfire project.)
Less than 48 hours from launch, the sandbox was opened and the secret was found. Congratulations to our bounty winners:
- Owen Kwan from Veria Labs who was the first to report a remote code execution to exfiltrate the
SECRETenvironment variable, and earns the $5k bounty. (More hack details on their blogpost) - Stanislav Fort from AISLE, who was the first to identify a weakness in Hack Monty's virtual filesystem. It wasn't sufficient to exfiltrate the secrets from the sandbox, but we consider it a real problem worth fixing, and we will pay out a supplementary $300 bounty.
As of Monty v0.0.16, the vulnerability first reported by Owen has been patched and Hack Monty's filesystem has been hardened against the patterns reported by Stanislav.
With the vulnerability patched, we have more confidence than ever that Monty is a secure sandbox for AI. The following sections of this blog explain how the vulnerability arose and why we believe there are no further vulnerabilities of this class remaining in Monty.
We plan to diligently analyze all the data from Hack Monty before we start Hack Monty 2; we take security extremely seriously at Pydantic and we look forward to putting a further hardened Monty out for you to break again soon!
Engineering a secure sandbox for AI
Monty is built to be secure by design:
- It is implemented in Rust, which is widely recognised for eliminating memory-safety bugs which are classic causes of sandbox escape. We use
unsafeRust only at critical points where it matters for Monty's overall design, and scrutinize this code carefully. The rest of the interpreter becomes safe by construction. - Monty implements a single-threaded subset of Python. This subset allows for predictability and mitigates the possibility of untrusted code being loaded into Monty.
- All actions heading outbound from the sandbox are mediated by the harness. The harness see every action that Monty can take and decides whether to permit it (and this is ultimately where we tightened up Hack Monty following Stanislav's report mentioned above).
With this in mind, it's reasonable to ask where the vulnerability arose from. The answer has two bugs working together - unsafe rust with an unsatisfied soundness requirement, plus an issue in Monty's GC which gave security researchers the opportunity to exploit the flawed unsafe code.
The vulnerability makes a great example of how Rust enables secure engineering by design and how unsafe Rust allows scrutiny to be focused on small sections of critical code, even though in this case the unsafe Rust was ultimately not correct.
Vulnerability step 1: the flawed unsafe code
Being an implementation of Python, Monty has to create Python objects which hold references to each other in a graph structure. Graph data structures are a challenging fit for Rust's borrow checker, because (under a direct application of Rust's Sharing XOR Mutability) editing data in one portion of the graph requires exclusive access to the whole graph. This would imply for example that the Monty interpreter cannot perform basic Python operations like list.extend(iterable) because the interpreter could not be writing to list while also reading from iterable.
There are ways to work around this without reaching for unsafe. Rust provides built-in types such as RefCell and Mutex which enable so-called "interior mutability", i.e. fine-grained mutability on individual elements of the graph without needing to take exclusive access to the whole graph. We evaluated these options, but they come with downsides of code complexity and runtime cost. For Monty we had the insight that we could use certain assumptions about Python objects, such that they don't change type once created, and the fact that Monty is a single-threaded implementation, to build our own way to enforce sharing XOR mutability at the individual element level on the graph.
This custom approach required unsafe code internally, and we took best practices to engineer the unsafe code, including testing the code with Rust's Miri interpreter and documenting all the safety invariants at the unsafe code points. Unfortunately, the documented safety invariants failed to account for the possibility that a flawed implementation of Monty's GC could violate those invariants. Due to the nature of unsafe code, once the invariants are broken, Rust's safety guarantees are void and it only becomes a matter of time before an exploit is found.
In this case, researchers were able to use the GC to exploit list.sort(key=<some function>) - an appropriately crafted key function could cause the GC to remove the list object undergoing sorting and replace it with an attacker controlled object, such as a bytes object with custom content. This is a use after free - a classic way for hackers to cause programs to execute attacker controlled code.
It's worth noting that without the unsafe code, this flaw in the GC would likely have led Monty to panic when the list object was replaced with another type. A panic typically leads to a noisy but deterministic shutdown, which at worst can lead to denial of service attacks rather than remote code execution.
Vulnerability step 2: weaknesses in Monty's GC
Like CPython, Monty primarily uses reference counting to keep track of Python objects and decide when they are no longer in use and can be safely cleaned up.
Reference counting is insufficient because objects can create cycles, for example the following Python code creates a cycle of two lists containing each other:
a = []
b = [a]
a.append(b)
Such cycles will never have their reference counts naturally drop to 0, so a second strategy is needed to detect these cycles and clean them up. The GC implementation in Monty is categorised as a tracing GC, which means that starting from a known set of "root" objects the GC identifies all objects which can be reached from those roots, with the conclusion that any object not reachable must be in a dangling cycle and so targetable for removal.
We learned from Hack Monty that Monty's tracing GC had several deficiencies, including:
- objects were missing from the root set, and
- objects had incorrect traversal logic.
These kinds of deficiencies meant that Monty's GC would not be able to reach all objects which it should have. Those objects were therefore incorrectly treated as being in dangling cycles and freed while still in use.
In the vulnerability exploited in Hack Monty where list.sort was attacked, this was because the list object undergoing sorting was missing from the set of roots and so could be incorrectly freed by the GC in this way.
Again, it's worth noting that without the flawed unsafe code above, this GC flaw still would have led to highly observable bugs in Monty such as panics or incorrect execution of the input Python code, but they would almost certainly not have led to remote code execution.
Closing the gap
Once the vulnerability was understood, we were able to immediately close the gap by patching the GC to include all objects currently dependent on the unsafe code's soundness in the root set. This restores the invariants needed by the unsafe code,
With this fix in place, and having conducted a thorough re-audit of the small number of unsafe blocks within Monty, we have confidence that there are no further memory safety issues within Monty.
This of course doesn't mean that we are complacent; we would be surprised if Monty is bug-free, we simply expect these to be minor logical bugs rather than vulnerabilities. For example, we know the current GC design doesn't prevent missing roots by construction, and we're exploring how we solve that, ideally before Hack Monty 2.
In conclusion
We thank everyone who participated in Hack Monty, and congratulate again our winners Owen and Stanislav.
Although the finding of a vulnerability is not the outcome we hoped for, the fact that this was the single issue found after over a million attacks on the Hack Monty API is a testament to the design choices involved in building Monty.
We look forward to bringing you Hack Monty 2 soon, with an even more secure Monty and a bigger bounty to boot!