Dev.to Security 🔐 Cybersecurity 👁 0 📖 6 min read

I Took a Memory Leak Arena to Google Ananta. It Got Picked

A pool of developers waiting for registration to pick up the esteemed, albeit temporary, ID tags, the ceiling about as high as the ambitions in the room. Possibly the only place I felt like I belonged, and had yearned fo

I Took a Memory Leak Arena to Google Ananta. It Got Picked

A pool of developers waiting for registration to pick up the esteemed, albeit temporary, ID tags, the ceiling about as high as the ambitions in the room. Possibly the only place I felt like I belonged, and had yearned for, for so long.

That was Saturday, 24 October 2026, at Google Ananta in Bengaluru, for FORK. BUILD. MERGE., hosted by GDG Cloud Bengaluru. I brought two open-source tools aimed at one problem: AI agents that share long-term memory, and leak it to each other.

my photo at Google Ananta

The problem, in one hotel

Picture a hotel where every room key opens every door, and the only thing keeping you out of the next room is a polite sign on it. That is how a lot of agent memory works today. Every user's memories sit in one shared vector table, and the boundary between them is a WHERE clause the application has to remember to write, on every query, in every code path, for as long as the product lives.

Similarity search doesn't read the sign. It ranks by distance, not by owner, so a perfectly innocent question about your own roadmap can pull up a colleague's private note that happens to sound like it, and no attacker is needed for that to happen. The MemLeak paper measured this kind of leak in multi-tenant agent memory with no attacker involved at all. Add the ordinary bugs on top: a filter someone forgot, a user id read from the prompt instead of the session, a sub-agent reading memos above its clearance.

My position is that the boundary belongs in the database, not in the app. To be fair, an application filter is simple, and it is right almost every time. Almost is the problem, because the one query that forgets it is the one that leaks.

What I brought

adk-memshield is a drop-in memory service for Google's Agent Development Kit (ADK) that moves the ownership check into PostgreSQL. It uses FORCE ROW LEVEL SECURITY, a database role that cannot bypass it, and a per-transaction identity: the user from the ADK session, the tenant and clearance from the service's own config. The design goal: a query with a missing or wrong filter returns fewer rows, not someone else's rows. It is on PyPI as adk-memshield.

adk-leak-arena is the red-team harness that tries to break it. It runs two real ADK Runners side by side on the same seeded data, one on a plain pgvector table guarded by application filters and one on adk-memshield, attacks both with five cross-boundary scenarios that go from incidental overlap all the way to a query with no WHERE clause at all, plus one same-tier boundary scenario, and shows the rows each side returned with the PostgreSQL telemetry underneath, so you can watch the boundary hold or fail query by query. Its GitHub repo dates from event day, and it reached PyPI the day after, as adk-leak-arena.

 the arena console, scenario S5 with no WHERE clause

What the numbers say, and how far they go

The arena's benchmark has six scenarios plus a benign control, 54 queries in all. Forty of them are cross-boundary attacks, eight in each of the scenarios S1 to S5, asked as one analyst with clearance 1, top three results per query.

  • Unprotected pgvector: 39 of the 40 attack queries returned at least one row the caller is not allowed to read (97.5%).
  • adk-memshield: 0 of 40.
  • The legitimate answer, where the caller had one, made the top three in 19 of 22 queries on the unprotected table and in 22 of 22 with adk-memshield. Leaked rows pushed it out once, and a spoofed owner filter returned only the victim's rows twice.

A rerun with gemma3:4b through Ollama gave the same leak and recall numbers. Good. They are measured on what the search returns, not on what the model says.

At 2,000 rows, the query plan shows where the work happens. RLS filters 2,000 rows down to the 45 the caller may read before any distance is computed; with fresh statistics, the scope index narrows to the tenant's 978 rows first.

the benchmark table

Now the limits, because a security number without its limits is just marketing. The dataset is synthetic and hand-written, 36 rows across three tenants, which means these percentages show you failure modes you can reproduce on your own machine and argue with, not real-world leak rates. One scenario is a known boundary, too. If someone at your own clearance writes a poisoned note into shared memory you are entitled to read, RLS returns it, because the read is authorized. Row-level security decides who may read a row, not whether the row is true, and that part needs write approval or moderation in the application.

Every number above has its command, commit and raw output in the arena's BENCHMARKS.md. Both test suites pass on those commits: 8 integration tests for adk-memshield, 45 end-to-end tests for the arena.

The bug that looked like a pass

Two days before the event, an adversarial review I'd set my coding agents to run threw malformed requests at the arena's API. One of them hid a NUL byte inside a spoofed user id, in the scenario where the unprotected side takes the user id straight from a tool argument. It came back HTTP 200, with zero rows.

Zero rows is a plausible answer for a mangled user id, so on its face nothing looked wrong. Why was it a fake pass, then? Because the telemetry under the pane was empty and the container log held an exception: three reasonable layers had lined up. The database driver, psycopg, refused the parameter before it ever reached PostgreSQL, since text fields cannot hold a NUL byte. ADK's PreloadMemoryTool caught that exception, logged a warning and let the turn continue with no memory. The arena then classified an empty result and counted zero leaked rows. A crash, reported as a security pass.

The arena now reports that case as a retrieval error instead of a verdict, and rejects NUL bytes at the API before they get near SQL. Upstream, I opened google/adk-python#7723. It adds a raise_on_error option and an on_error callback to PreloadMemoryTool, so an application can fail closed, or at least find out. The defaults keep today's behavior, and the PR is open, waiting for review. One honest gap remains in my own tool: its leak percentages still count an errored query as not leaking, and the CLI and Markdown tables don't show the separate error count (the JSON report and the web console do). Every published run had zero errors.

The people

Krishna Mehta hosted and led the organizing. Pratik Singh, one of the judges, from GitLab, watched the leak-arena demo with good humor and stories from his own work, and we swapped notes on the week's CVE news.

The result

At the end, the organizers announced 10 projects, in no particular order, with goodie bags. Mine was one of them: one of 10 projects picked, out of 211 PRs raised that day (by my count).

adk-leak-arena had zero GitHub stars when I checked this morning, so for now this post is its entire marketing department.

Try it

If you run agents that share memory, add your own retrieval pattern to the arena as a target and attack it before someone else does.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.