PLEX-3289: pollOneoff fix -- prevent memory amplification - #2299
Conversation
✅ API Diff Results -
|
There was a problem hiding this comment.
Pull request overview
This PR introduces a configurable upper bound for the WASI poll_oneoff host call’s nsubscriptions argument to prevent guest-controlled memory amplification during subscription/event buffer handling in the WASM host runtime.
Changes:
- Add
MaxSubscriptionsLimitertoModuleConfigand default it from a newcresettingsvalue. - Enforce the subscription limit in both the legacy-DAG WASI host implementation and the execution runtime implementation.
- Thread a
context.Contextinto the legacy-DAG WASI linker so the limiter can be evaluated with the correct execution context.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/workflows/wasm/host/wasip1.go | Wrap legacy-DAG poll_oneoff with a limiter-driven bound and plumb context.Context into the DAG linker. |
| pkg/workflows/wasm/host/module.go | Add MaxSubscriptionsLimiter to module configuration, initialize default limiter, and pass context into linking. |
| pkg/workflows/wasm/host/execution.go | Replace the previous nsubscriptions bound with a limiter-based bound in execution.pollOneoff. |
| pkg/settings/cresettings/settings.go | Add WASMPollOneoffSubscriptionLimit to settings schema and defaults. |
| pkg/settings/cresettings/README.md | Document the new setting in the settings flowchart. |
| pkg/settings/cresettings/defaults.toml | Provide default value for WASMPollOneoffSubscriptionLimit. |
| pkg/settings/cresettings/defaults.json | Provide default value for WASMPollOneoffSubscriptionLimit. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (4)
pkg/workflows/wasm/host/execution.go:365
nsubscriptions > int32(maxSubscriptions)risks int32 overflow if the configured limit exceeds MaxInt32 (the cast can wrap). Sincensubscriptionsis already bounded to a safe range, prefer comparing withint(nsubscriptions)against the int limit.
if nsubscriptions <= 0 ||
nsubscriptions > int32(maxSubscriptions) ||
nsubscriptions > max(math.MaxInt32/subscriptionLen, math.MaxInt32/eventsLen) {
return ErrnoInval
pkg/workflows/wasm/host/wasip1.go:150
- The comparison
nsubscriptions > int32(maxSubscriptions)can overflow if the configured limit exceeds MaxInt32, causing the cast to wrap and potentially rejecting valid requests unexpectedly. Sincensubscriptionsis already bounded to a safe int32 range, compare by convertingnsubscriptionsto int instead.
if nsubscriptions <= 0 ||
nsubscriptions > int32(maxSubscriptions) ||
nsubscriptions > max(math.MaxInt32/subscriptionLen, math.MaxInt32/eventsLen) {
return ErrnoInval
pkg/workflows/wasm/host/wasip1.go:147
- The new MaxSubscriptionsLimiter guard is a key safety control for preventing poll_oneoff memory amplification; it should be covered by a test that asserts (1) values above the limit return ErrnoInval and (2) the call does not allocate/write events for out-of-range inputs.
This issue also appears on line 147 of the same file.
maxSubscriptions, err := cfg.MaxSubscriptionsLimiter.Limit(ctx)
if err != nil {
return ErrnoInval
}
if nsubscriptions <= 0 ||
pkg/workflows/wasm/host/execution.go:362
- The new MaxSubscriptionsLimiter enforcement in pollOneoff should have regression coverage to ensure oversized nsubscriptions is rejected (ErrnoInval) and to guard against reintroducing large guest-controlled allocations.
This issue also appears on line 362 of the same file.
maxSubscriptions, err := e.module.cfg.MaxSubscriptionsLimiter.Limit(e.ctx)
if err != nil {
return ErrnoInval
}
if nsubscriptions <= 0 ||
f0308a3 to
28919fe
Compare
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 2.
| Benchmark suite | Current: 4a6d855 | Previous: 874889e | Ratio |
|---|---|---|---|
BenchmarkKeystore_Sign/nop/in-process |
691.3 ns/op |
285.2 ns/op |
2.42 |
This comment was automatically generated by workflow using github-action-benchmark.
28919fe to
c14023a
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (2)
pkg/workflows/wasm/host/wasip1.go:189
- The clock subscription parsing comment says the flag is at bytes 24-32, but it’s being decoded as a uint16. Tightening the slice and correcting the comment avoids confusion and matches the (u16 + padding) layout. (Consider mirroring the same change in execution.pollOneoff for consistency.)
// - 24-32: flag
newTimeout := binary.LittleEndian.Uint64(argBuf[8:16])
flag := binary.LittleEndian.Uint16(argBuf[24:32])
pkg/workflows/wasm/host/poll_oneoff_regression_test.go:19
- The comment has a duplicated word ("for for"), which reads like a typo.
// This means for for every subscription created in the host, we'd allocate 1.67 times
c14023a to
2e957d1
Compare
2e957d1 to
4a6d855
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (2)
pkg/workflows/wasm/host/poll_oneoff_regression_test.go:19
- Spelling/grammar: the comment has a duplicated "for" ("means for for every").
// This means for for every subscription created in the host, we'd allocate 1.67 times
pkg/workflows/wasm/host/wasip1.go:166
- The subscription argument buffer is currently sliced to the end of the
subsbacking array, which makesargBufinclude bytes from subsequent subscriptions. While the current fixed offsets happen to work, this is brittle and makes it easier to accidentally read across subscription boundaries if the parsing logic changes. SliceargBufto the current subscription’s range instead.
eventType := subs[inOffset+8]
argBuf := subs[inOffset+8+8:]
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (2)
pkg/workflows/wasm/host/poll_oneoff_regression_test.go:20
- The comment has a duplicated word (“for for”), which reads like a typo.
// copies the subscriptions into Go's heap and allocates a proportional buffer for events.
// This means for for every subscription created in the host, we'd allocate 1.67 times
// that much memory on the host. We fix this by rejecting calls with large nsubscriptions.
pkg/settings/cresettings/settings.go:365
- The Schema comment says the Go runtime “never needs more than one”, but the default is set to 128. This is confusing as written; either the default should be lowered to 1, or the comment should explain why the default allows more than one (e.g., headroom for non-Go guests / future runtime changes).
// WASMPollOneoffSubscriptionLimit bounds nsubscriptions in the WASI
// poll_oneoff host call. Checked against the Go wasip1 runtime
// (https://cs.opensource.google/go/go/+/refs/tags/go1.26.2:src/runtime/netpoll_wasip1.go),
// which never needs more than one.
WASMPollOneoffSubscriptionLimit Setting[int] `unit:"{subscription}"`
Requires
Supports