[BUG] fatal error: concurrent map read and map write
Type: Bug
### Is there an existing issue for this?
- [x] I have searched the existing issues.
### Current Behavior
Nuclei crashes with a Go runtime fatal (`concurrent map read and map write`) when a flow template executes a JS protocol step under load. The crash originates inside the `goja` JS engine's `templatedObject` while
the flow executor is invoking a JS protocol request:
```
nuclei/pkg/tmplexec/flow.(*FlowExecutor).requestExecutor
→ nuclei/pkg/protocols/javascript.(*Request).ExecuteWithResults (js.go:296)
→ nuclei/pkg/js/compiler.(*Compiler).ExecuteWithOptions (compiler.go:121)
→ nuclei/pkg/js/compiler.executeWithRuntime (pool.go:85)
→ goja.(*Runtime).RunProgram
→ ... goja vm ...
→ goja.(*Object).Delete (runtime.go:1833)
→ goja.(*templatedObject).deleteStr (object_template.go:249)
→ goja.(*templatedObject).getOwnPropStr (object_template.go:109) ← FATAL
```
The fatal map error is non-recoverable — the entire process dies.
### Root cause analysis
`templatedObject.getOwnPropStr` lazily materialises template-backed properties into the per-instance `o.values` map without locking:
https://github.com/Mzack9999/goja/blob/e46100e9c697/object_template.go#L108-L118
```go
func (o *templatedObject) getOwnPropStr(p unistring.String) Value {
if v, exists := o.values[p]; exists { // read
return v
}
if f := o.tmpl.props[p]; f != nil {
v := f(o.val.runtime)
o.values[p] = v // write — first-touch materialisation
return v
}
return nil
}
```
deleteStr then reads the same map again. None of baseObject.values access in goja is locked — by design. goja's documented concurrency model is one goroutine per *goja.Runtime, and templatedObject is one of many
places that assume it.
The fatal therefore proves that two goroutines are operating on the same *goja.Runtime concurrently. The most plausible source is pkg/js/compiler/pool.go checking out a runtime that is still in use by another
goroutine — most likely because a flow template's JS callback dispatches work that re-enters the JS pool, or because the pool returns the same runtime to two callers.
### Expected Behavior
Nuclei should not crash.
### Steps To Reproduce
Hard to reproduce or maybe with `-race` ?
### Relevant log output
```shell
fatal error: concurrent map read and map write
goroutine 9800382 [running]:
internal/runtime/maps.fatal({0x32764ae?, 0x0?})
runtime/panic.go:1181 +0x18
github.com/Mzack9999/goja.(*templatedObject).getOwnPropStr(0x7940fbeec00, {0x318d975, 0x8})
github.com/Mzack9999/goja@v0.0.0-20250507184235-e46100e9c697/object_template.go:109 +0x3a
github.com/Mzack9999/goja.(*templatedObject).deleteStr(0x7940fbeec00, {0x318d975, 0x8}, 0x1)
github.com/Mzack9999/goja@v0.0.0-20250507184235-e46100e9c697/object_template.go:249 +0x2b
github.com/Mzack9999/goja.(*Object).Delete.func1()
github.com/Mzack9999/goja@v0.0.0-20250507184235-e46100e9c697/value.go:916 +0x4c
github.com/Mzack9999/goja.(*vm).try(0x79416c70fc0, 0x7940482c3a8)
github.com/Mzack9999/goja@v0.0.0-20250507184235-e46100e9c697/vm.go:864 +0x21b
github.com/Mzack9999/goja.(*Runtime).try(...)
github.com/Mzack9999/goja@v0.0.0-20250507184235-e46100e9c697/runtime.go:2648
github.com/Mzack9999/goja.(*Object).Delete(0x1?, {0x318d975?, 0x793fd6319a8?})
github.com/Mzack9999/goja@v0.0.0-20250507184235-e46100e9c697/value.go:915 +0x45
github.com/Mzack9999/goja.(*Runtime).toValue.func1({{0x3abd230, 0x6b197e0}, {0x793fed5c560, 0x0, 0x2}})
github.com/Mzack9999/goja@v0.0.0-20250507184235-e46100e9c697/runtime.go:1833 +0x3d
github.com/Mzack9999/goja.(*nativeFuncObject).vmCall(0x7940280c1e0, 0x793fdeb4fc0, 0x0)
github.com/Mzack9999/goja@v0.0.0-20250507184235-e46100e9c697/func.go:563 +0x184
github.com/Mzack9999/goja.call.exec(0xa?, 0x793fdeb4fc0)
github.com/Mzack9999/goja@v0.0.0-20250507184235-e46100e9c697/vm.go:3642 +0x66
github.com/Mzack9999/goja.(*vm).run(0x793fdeb4fc0)
github.com/Mzack9999/goja@v0.0.0-20250507184235-e46100e9c697/vm.go:635 +0x5b
github.com/Mzack9999/goja.(*vm).runTryInner(0x79424e70910?)
github.com/Mzack9999/goja@v0.0.0-20250507184235-e46100e9c697/vm.go:887 +0x52
github.com/Mzack9999/goja.(*vm).runTry(0x793fdeb4fc0)
github.com/Mzack9999/goja@v0.0.0-20250507184235-e46100e9c697/vm.go:873 +0x1b4
github.com/Mzack9999/goja.(*Runtime).RunProgram(0x7940e1b6008, 0x794370e59a0)
github.com/Mzack9999/goja@v0.0.0-20250507184235-e46100e9c697/runtime.go:1476 +0x425
github.com/projectdiscovery/nuclei/v3/pkg/js/compiler.executeWithRuntime.func1()
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/js/compiler/pool.go:85 +0x4d
github.com/projectdiscovery/nuclei/v3/pkg/js/compiler.executeWithRuntime(...)
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/js/compiler/pool.go:134 +0x595
github.com/projectdiscovery/nuclei/v3/pkg/js/compiler.executeWithPoolingProgram(...)
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/js/compiler/pool.go:218 +0x42e
github.com/projectdiscovery/nuclei/v3/pkg/js/compiler.ExecuteProgram(...)
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/js/compiler/pool.go:152 +0xa9
github.com/projectdiscovery/nuclei/v3/pkg/js/compiler.(*Compiler).ExecuteWithOptions(...)
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/js/compiler/compiler.go:121 +0x38f
github.com/projectdiscovery/nuclei/v3/pkg/protocols/javascript.(*Request).executeRequestWithPayloads(...)
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/protocols/javascript/js.go:563 +0x445
github.com/projectdiscovery/nuclei/v3/pkg/protocols/javascript.(*Request).executeWithResults(...)
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/protocols/javascript/js.go:456 +0xcff
github.com/projectdiscovery/nuclei/v3/pkg/protocols/javascript.(*Request).ExecuteWithResults(...)
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/protocols/javascript/js.go:296 +0x105
github.com/projectdiscovery/nuclei/v3/pkg/tmplexec/flow.(*FlowExecutor).requestExecutor(...)
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/tmplexec/flow/flow_internal.go:41 +0x306
github.com/projectdiscovery/nuclei/v3/pkg/tmplexec/flow.(*FlowExecutor).Compile.func1(...)
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/tmplexec/flow/flow_executor.go:174 +0x1d4
... goja vm frames ...
github.com/projectdiscovery/nuclei/v3/pkg/tmplexec/flow.(*FlowExecutor).ExecuteWithResults(...)
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/tmplexec/flow/flow_executor.go:255 +0x551
github.com/projectdiscovery/nuclei/v3/pkg/tmplexec.(*TemplateExecuter).Execute(...)
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/tmplexec/exec.go:211 +0x3df
github.com/projectdiscovery/nuclei/v3/pkg/core.(*Engine).executeTemplateOnInput(...)
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/core/executors.go:266 +0x137
github.com/projectdiscovery/nuclei/v3/pkg/core.(*Engine).executeTemplateWithTargets.func2.1(...)
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/core/executors.go:111 +0xe5
github.com/projectdiscovery/nuclei/v3/pkg/core.(*Engine).executeTemplateWithTargets.func2()
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/core/executors.go:116 +0x112
created by github.com/projectdiscovery/nuclei/v3/pkg/core.(*Engine).executeTemplateWithTargets in goroutine 9800381
github.com/projectdiscovery/nuclei/v3@v3.8.0/pkg/core/executors.go:97 +0x2e8
```
### Environment
```markdown
- OS: Linux
- Nuclei: v3.8.0
- Go: 1.26
```
### Anything else?
The Mzack9999/goja fork has issues disabled, and upstream dop251/goja shows no commit touching object_template.go since the original templatedObject implementation. Any fix that doesn't redesign goja's concurrency model has to live in Nuclei's JS runtime pool.
0 条评论