You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Source bug:proxy-wasm/proxy-wasm-cpp-host#326 — The shared delay queue of doAfterVmCallActions will queue the request phase into continueDecoding when sendLocalReply → onResponseHeaders re-enters. Drain it in the response phase (stage misalignment execution).
This proposal is the official follow-up to SPEC-003 / QUESTION-002 on #4064. Separated from #4034 (CPU infinite loop): Layer A (drain-to-local) of #4064not fixed#326, needs to be changed separately.
**This iteration only does project establishment/scoping and does not implement repairs. ** No design/implement/TASK artifact. SPEC/QUESTION is hung under this issue as a typed comment.
#326 is a real bug and different from #4034** - the symptom is cross-stage callback drain (stage-mismatch), not a CPU infinite loop. Confirmed through fork pinned code tracking:
The root cause is queue scope + stageless filtering: after_vm_call_actions_ is a per-VM shared std::deque on WasmBase, ~DeferAfterCallActions unconditionally drains, and the same Context instance serves both decode and encode phases** - so changing the queue to per-context cannot fix it (escape actions and drain share the same context).
Therefore #326 requires its own change. This iteration is only project establishment, only scoping, not implementation. It is designed in collaboration with the upstream #326 thread and will be implemented after Layer A (#4064/#4070) is implemented.
Why (root cause tracking, file: line)
Deferred action queue after_vm_call_actions_ is a member of WasmBase (per-VM shared, not per-context) - proxy-wasm-cpp-host include/proxy-wasm/wasm.h:338; enqueue/drain logic is in wasm.h:158-169.
DeferAfterCallActions(ContextBase*) discards context during construction and only retains WasmBase*** (include/proxy-wasm/context.h:455); ~DeferAfterCallActionsunconditionally calls wasm_->doAfterVmCallActions() (src/context.cc:51-54).
The request/decoder phase will enqueue sendLocalReply (higress-group/envoy source/extensions/common/wasm/context.cc:2058)** and continueDecoding (context.cc:1929`) into the same shared deque.
drain runs sendLocalReply first, which synchronously drivesencodeHeaders → onResponseHeaders (context.cc:2254) → triggers nested~DeferAfterCallActions → drain the same deque, so the request phase continueDecoding that is still queued is executed within the VM call in the **response phase → phase misalignment.
The same Context instance serves both decode (context.cc:2150) and encode (context.cc:2254) - this is why the per-context queue cannot be repaired: the escape action shares the same context as drain.
1. Stage-tagging (minimally defensible, still a MEDIUM risk) – leading candidate
Introduce the stage concept and filter by current drain stage. There is no ready-made phase enum that can be reused: ContextBase/envoy Context does not have a persistence phase - WasmStreamType{Request,Response,Downstream,Upstream} only exists as a parameter of continueStream and is never saved; the only thread local state is wasm_vm.h:382-387current_context_/effective_context_id_, that is context identity, not phase.
So the minimal defensible patch:
Added WasmBase::current_stage_, save/restore in DeferAfterCallActions (use auto-tag to keep about 20 addAfterVmCallAction call signatures unchanged);
Change after_vm_call_actions_ to deque<pair<stage, fn>>;
doAfterVmCallActions only executes actions whose tag matches the current drain stage, always executes stage-agnostic forced cleanup, and flushes all when teardown occurs;
Cost: ~3 pwh files + envoy fork at about 40 DeferAfterCallActions actions(this); construction points annotated + classification at about 20 enqueue call points + ~80 lines of logic.
2. Per-context queue (move deque to ContextBase) - Rejected
Does not fix #326: Escape action and drain share the same Context. Reject.
3. Terminal SendHttpResponse + suppress self-reply callback (PiotrSikora direction) - the most correct but most destructive
Make SendHttpResponse the final action (no wasm is run afterward), and never trigger onResponse* for the plugin's own local reply. Closest to the root cause, but destructive and on the Envoy side with the greatest scope for change.
Blast-radius honest assessment (stage-tagging)
This change is only partially controllable:
Mechanical OK: No wasm ABI changes → Plugins are not affected; about 40 Defer construction point changes are compiler catchable; orthogonal to Layer A.
Risk concentration in:
**(a) Classification risk: ** About 20 enqueue call points must be manually distinguished into "stage-sensitive continuation" vs "stage-independent forced cleanup" (handler-map erase of context.cc:1260/1287/2367/2398/2423/2465, shutdown-handle delete of pwh wasm.cc:532). Cleanup misclassification → resource leakage; continuation misclassification → request suspension; both are silent and timing-related, and are difficult to cover with a single test.
**(b) Dual warehouse collaboration: ** If pwh fork and envoy fork are involved at the same time, they must be modified, pinned and released at the same time (the collaboration cost is higher than single file changes in Layer A).
(c) teardown-flush is new behavior and requires specialized edge case testing.
**(d) Divergence from upstream wishes: ** Need to carry fork patch for a long time.
Interaction with Layer A: Orthogonal, safe to stack. The only co-constraint is that the stage filter must be compared to the current drain stage (restored between nests), not the stage at the enqueue time snapshot.
Upstream precedent (honest record)
Upstream PR proxy-wasm/proxy-wasm-cpp-host#423Closed and not merged. It does not implement stage-tagging, only changes the error propagation of send_local_response in exports.cc (a narrow symptom of envoyproxy/envoy#28826), and breaks the host-ABI contract (Rust SDK panics on errors). Maintainer PiotrSikora advocates that the correct fix is to (a) let SendHttpResponse terminate (not run wasm afterwards), and (b) never trigger onResponse* for the plugin's own local reply - both are destructive and on the Envoy side. Maintainer johnlanni proposed stage-tagging. **No consensus reached. **
Recommend stage-tagging "minimum defensible patch" as the leading candidate, but marked as medium-risk; and document the upstream terminal-SendHttpResponse alternative.
Scope
In (only design/decision-making this time, no coding):
Design and decisions of stage-correct drain in higress-group/proxy-wasm-cpp-host (+ enqueue annotation of envoy fork).
Deferred:
Actual implementation (stage-tagging patch itself) - pending design/implementation iteration of this change.
Out:
Envoy side terminal-SendHttpResponse redesign (upstream, destructive).
QUESTION (this issue) — Method revision route: stage-tagging (prototype in fork first) vs upstream terminal-reply redesign first? Non-blocking, the default is "prototype stage-tagging in fork first, and collaborate with the upstream Update the leveled-configuration model in the README files of Wasm plugins #326 thread before launching".
Push upstream first vs carry fork patches long term?
The backport target (release branch) is to be determined.
Proposal: 修复上游 #326 Wasm
doAfterVmCallActions跨阶段回调 drain — stage-correct drain inhigress-group/proxy-wasm-cpp-host关联项目
include/proxy-wasm/wasm.h、include/proxy-wasm/context.h、src/context.cc、src/wasm.ccsource/extensions/common/wasm/context.cc引用版本:pwh fork pinned commit
8549cf6374835d225edf67e584cb8d8d8a0fc256。关联工件
结论
#326 是一个真实且与 #4034 不同的 bug——症状是跨阶段回调 drain(stage-mismatch),不是 CPU 死循环。已通过 fork pinned 代码追踪确认:
WasmBase::doAfterVmCallActions(Layer A, #4034) #4070 的 drain-to-local)不修复 Update the leveled-configuration model in the README files of Wasm plugins #326——它让 drain 循环重入安全(终结 CPU spin),但保留了 FIFO drain 语义,请求阶段排入的动作照样会在响应阶段的 drain 里被执行。after_vm_call_actions_是WasmBase上的每-VM 共享std::deque,~DeferAfterCallActions无条件 drain,且同一个Context实例同时服务 decode 与 encode 阶段——所以把队列改成 per-context 也不能修复(逃逸动作与 drain 共用同一 context)。因此 #326 需要自己的 change。本次迭代只立项、只 scoping、不实现,与上游 #326 thread 协同设计,待 Layer A(#4064/#4070)落地后再实施。
Why(根因追踪,file:line)
after_vm_call_actions_是WasmBase的成员(每-VM 共享,非 per-context)——proxy-wasm-cpp-host include/proxy-wasm/wasm.h:338;enqueue/drain 逻辑在wasm.h:158-169。DeferAfterCallActions(ContextBase*)构造时丢弃 context、只保留WasmBase*(include/proxy-wasm/context.h:455);~DeferAfterCallActions无条件调用wasm_->doAfterVmCallActions()(src/context.cc:51-54)。sendLocalReply(higress-group/envoy source/extensions/common/wasm/context.cc:2058)和continueDecoding(context.cc:1929)排入同一个共享 deque。sendLocalReply,它同步驱动encodeHeaders → onResponseHeaders(context.cc:2254)→ 触发嵌套的~DeferAfterCallActions→ drain 同一个 deque,于是仍排在队列里的请求阶段continueDecoding在响应阶段的 VM call 内被执行 → 阶段错位。Context实例既服务 decode(context.cc:2150)又服务 encode(context.cc:2254)——这正是 per-context 队列无法修复的原因:逃逸动作与 drain 共用同一 context。Fix candidates(候选修法 + 排序 + blast-radius)
1. Stage-tagging(最小可辩护,仍属 MEDIUM 风险)— 领先候选
引入 stage 概念并按当前 drain stage 过滤。没有现成的 phase enum 可复用:
ContextBase/envoyContext上并未持久化阶段——WasmStreamType{Request,Response,Downstream,Upstream}只作为continueStream的参数存在、从不落盘;唯一的线程局部状态是wasm_vm.h:382-387的current_context_/effective_context_id_,那是 context 身份、不是 phase。因此最小可辩护补丁:
WasmBase::current_stage_,在DeferAfterCallActions里 save/restore(借助 auto-tag 保持约 20 处addAfterVmCallAction调用签名不变);after_vm_call_actions_改成deque<pair<stage, fn>>;doAfterVmCallActions只执行 tag 与当前 drain stage 匹配的动作,始终执行 stage-agnostic 的强制清理,teardown 时 flush 全部;成本: ~3 个 pwh 文件 + envoy fork 在约 40 处
DeferAfterCallActions actions(this);构造点做注解 + 分类约 20 处 enqueue 调用点 + ~80 行逻辑。2. Per-context 队列(把 deque 移到
ContextBase)— 拒绝不修复 #326:逃逸动作与 drain 共用同一个
Context。Reject。3. Terminal
SendHttpResponse+ 抑制 self-reply 回调(PiotrSikora 方向)— 最正确但破坏性最大让
SendHttpResponse成为终止操作(其后不再跑 wasm),并且永不为插件自身的 local reply 触发onResponse*。最贴近根因,但破坏性且在 Envoy 侧,改动面最大。Blast-radius 诚实评估(stage-tagging)
该改动只是部分可控:
context.cc:1260/1287/2367/2398/2423/2465的 handler-map erase、pwh wasm.cc:532的 shutdown-handle delete)。清理误分类 → 资源泄漏;续跑误分类 → 请求挂起;两者都是静默且时序相关,难以单测覆盖。Upstream 先例(诚实记录)
上游 PR proxy-wasm/proxy-wasm-cpp-host#423 已关闭未合并。它没有实现 stage-tagging,只改了
exports.cc里send_local_response的错误传播(envoyproxy/envoy#28826 的一个较窄症状),并破坏了 host-ABI 契约(Rust SDK 在 error 时 panic)。维护者 PiotrSikora 主张正确修法是 (a) 让SendHttpResponse终止化(其后不跑 wasm)、(b) 永不为插件自身 local reply 触发onResponse*——两者都破坏性且在 Envoy 侧。维护者 johnlanni 提议 stage-tagging。未达成共识。Decision(本次迭代)
WasmBase::doAfterVmCallActions(Layer A, #4034) #4070)落地之后再实施。SendHttpResponse替代方案。Scope
In(本次仅做设计 / 决策,不写码):
higress-group/proxy-wasm-cpp-host中 stage-correct drain 的设计与决策(+ envoy fork 的 enqueue 注解)。Deferred:
Out:
SendHttpResponse重设计(上游、破坏性)。WasmBase::doAfterVmCallActions(Layer A, #4034) #4070 已定)。Open questions
Proposal: Fix upstream #326 Wasm
doAfterVmCallActionscross-stage callback drain — stage-correct drain inhigress-group/proxy-wasm-cpp-hostRelated projects
include/proxy-wasm/wasm.h,include/proxy-wasm/context.h,src/context.cc,src/wasm.ccsource/extensions/common/wasm/context.ccReference version: pwh fork pinned commit
8549cf6374835d225edf67e584cb8d8d8a0fc256.Associated artifacts
Conclusion
#326 is a real bug and different from #4034** - the symptom is cross-stage callback drain (stage-mismatch), not a CPU infinite loop. Confirmed through fork pinned code tracking:
WasmBase::doAfterVmCallActions(Layer A, #4034) #4070) does not fix Update the leveled-configuration model in the README files of Wasm plugins #326 - it makes drain loop reentrancy safe (terminating CPU spin), but retains FIFO drain semantics, and actions queued in the request phase will still be executed in the drain in the response phase.after_vm_call_actions_is a per-VM sharedstd::dequeonWasmBase,~DeferAfterCallActionsunconditionally drains, and the sameContextinstance serves both decode and encode phases** - so changing the queue to per-context cannot fix it (escape actions and drain share the same context).Therefore #326 requires its own change. This iteration is only project establishment, only scoping, not implementation. It is designed in collaboration with the upstream #326 thread and will be implemented after Layer A (#4064/#4070) is implemented.
Why (root cause tracking, file: line)
after_vm_call_actions_is a member ofWasmBase(per-VM shared, not per-context) -proxy-wasm-cpp-host include/proxy-wasm/wasm.h:338; enqueue/drain logic is inwasm.h:158-169.DeferAfterCallActions(ContextBase*)discards context during construction and only retainsWasmBase*** (include/proxy-wasm/context.h:455);~DeferAfterCallActionsunconditionally callswasm_->doAfterVmCallActions()(src/context.cc:51-54).sendLocalReply(higress-group/envoy source/extensions/common/wasm/context.cc:2058)** and continueDecoding(context.cc:1929`) into the same shared deque.sendLocalReplyfirst, which synchronously drivesencodeHeaders → onResponseHeaders(context.cc:2254) → triggers nested~DeferAfterCallActions→ drain the same deque, so the request phasecontinueDecodingthat is still queued is executed within the VM call in the **response phase → phase misalignment.Contextinstance serves both decode (context.cc:2150) and encode (context.cc:2254) - this is why the per-context queue cannot be repaired: the escape action shares the same context as drain.Fix candidates (candidate fix + sort + blast-radius)
1. Stage-tagging (minimally defensible, still a MEDIUM risk) – leading candidate
Introduce the stage concept and filter by current drain stage. There is no ready-made phase enum that can be reused:
ContextBase/envoyContextdoes not have a persistence phase -WasmStreamType{Request,Response,Downstream,Upstream}only exists as a parameter ofcontinueStreamand is never saved; the only thread local state iswasm_vm.h:382-387current_context_/effective_context_id_, that is context identity, not phase.So the minimal defensible patch:
WasmBase::current_stage_, save/restore inDeferAfterCallActions(use auto-tag to keep about 20addAfterVmCallActioncall signatures unchanged);after_vm_call_actions_todeque<pair<stage, fn>>;doAfterVmCallActionsonly executes actions whose tag matches the current drain stage, always executes stage-agnostic forced cleanup, and flushes all when teardown occurs;Cost: ~3 pwh files + envoy fork at about 40
DeferAfterCallActions actions(this);construction points annotated + classification at about 20 enqueue call points + ~80 lines of logic.2. Per-context queue (move deque to
ContextBase) - RejectedDoes not fix #326: Escape action and drain share the same
Context. Reject.3. Terminal
SendHttpResponse+ suppress self-reply callback (PiotrSikora direction) - the most correct but most destructiveMake
SendHttpResponsethe final action (no wasm is run afterward), and never triggeronResponse*for the plugin's own local reply. Closest to the root cause, but destructive and on the Envoy side with the greatest scope for change.Blast-radius honest assessment (stage-tagging)
This change is only partially controllable:
context.cc:1260/1287/2367/2398/2423/2465, shutdown-handle delete ofpwh wasm.cc:532). Cleanup misclassification → resource leakage; continuation misclassification → request suspension; both are silent and timing-related, and are difficult to cover with a single test.Upstream precedent (honest record)
Upstream PR proxy-wasm/proxy-wasm-cpp-host#423 Closed and not merged. It does not implement stage-tagging, only changes the error propagation of
send_local_responseinexports.cc(a narrow symptom of envoyproxy/envoy#28826), and breaks the host-ABI contract (Rust SDK panics on errors). Maintainer PiotrSikora advocates that the correct fix is to (a) letSendHttpResponseterminate (not run wasm afterwards), and (b) never triggeronResponse*for the plugin's own local reply - both are destructive and on the Envoy side. Maintainer johnlanni proposed stage-tagging. **No consensus reached. **Decision (this iteration)
WasmBase::doAfterVmCallActions(Layer A, #4034) #4070) is implemented.SendHttpResponsealternative.Scope
In (only design/decision-making this time, no coding):
higress-group/proxy-wasm-cpp-host(+ enqueue annotation of envoy fork).Deferred:
Out:
SendHttpResponseredesign (upstream, destructive).WasmBase::doAfterVmCallActions(Layer A, #4034) #4070 finalized).Open questions