Skip to content

Make Slab safe to use from multiple threads - #344

Merged
alexcrichton merged 2 commits into
bytecodealliance:mainfrom
barisa-cloudsmith:fix-slab-handle-aliasing
Sep 24, 2026
Merged

alexcrichton merged 2 commits into
bytecodealliance:mainfrom
barisa-cloudsmith:fix-slab-handle-aliasing

Conversation

@barisa-cloudsmith

Copy link
Copy Markdown
Contributor

We started using wasmtime-py from multiple treads and noticed that we are getting memory corruptions.
As a fix, changed non thread safe usage of lists for tracking memory slabs, to dict + counter, which is thread safe.

We already have an internal monkey patch with this behavior, so we are hoping to either merge this functionality upstream, or to learn if our approach has some gaps.

wasmtime objects free their `Slab` handles from finalizers, and the garbage
collector runs finalizers on whichever thread triggers a collection. So an
application that calls wasmtime from one thread, while other threads just
allocate Python objects (a boto upload, in our case), gets `deallocate`
running on those other threads at the same time as `allocate` on the
wasmtime thread.

The old `Slab` can't handle that. Its free list lives in the same array as
its values, and both `allocate` and `deallocate` update it across several
statements. If a thread switch lands in the middle, one handle goes to two
owners, and later the slab is poisoned for good:

    TypeError: list indices must be integers or slices, not tuple

Get rid of the free list. Handles come from a counter and values live in a
dict, so there's no multi-statement state left to interleave. A lock guards
only the counter (`next()` on `itertools.count` isn't atomic on
free-threaded builds). The lock is never held while a value is dropped, so a
finalizer that frees another handle can't deadlock. Handles start at 1, so a
null `void *` env read back as `idx or 0` can never match a live handle.

The new test runs allocate/deallocate from several threads. Against the old
`Slab` it fails every time, with an aliased handle or the `TypeError` above.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@alexcrichton alexcrichton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks! I've always historically hoped there's a better solution here but I'm not sure there is one...

As a comment on the implementation though, could the implementation largely stay the same way it was but with a lock around allocate/deallocate? I don't think it should be necessary to switch to a dictionary and a counter iterator in theory.

@barisa-cloudsmith

Copy link
Copy Markdown
Contributor Author

Hi @alexcrichton, thnx for a quick feedback.

Main reason I chose a different structure was so I wouldn't need to use lock/unlock around allocate/deallocate, so there wouldn't be any performance loss.

If some perf loss is okey, I'm happy to change the impl.
Are you aware of any downside of using dict/counter instead?

@alexcrichton

Copy link
Copy Markdown
Member

Oh I hadn't really considered perf loss here, I was mostly just thinking of minimizing the diff and naively to me arrays feel faster than dicts, but I've no idea if that's actually the case. I'd lean towards the array-based solution as I'd suspect in the end it'd be more efficient, but if you feel differently this strategy seems reasonable too

@barisa-cloudsmith

barisa-cloudsmith commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor Author

I created a small benchmark script, with single and multiple threads, check out how it looks for your setup
Since I'm using normal python, there are no improvements when using multiple threads

ns per get+deallocate+allocate (best of 5, 1,000,000 ops)
 threads  list+lock       dict  speedup
       1        243        114    2.13x
       4        249        116    2.15x

benchmark script code:
https://gist.github.com/barisa-cloudsmith/53b99d878331cb833e238abdc0ea1780

@alexcrichton alexcrichton left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That's quite the difference!

@alexcrichton
alexcrichton merged commit 280ce73 into bytecodealliance:main Sep 24, 2026
15 checks passed
@barisa-cloudsmith
barisa-cloudsmith deleted the fix-slab-handle-aliasing branch September 24, 2026 20:54
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.

2 participants