Skip to content

fix: prevent concurrent map writes in MultiPartForm.Decode#7240

Open
VoidChecksum wants to merge 2 commits intoprojectdiscovery:devfrom
VoidChecksum:fix/concurrent-map-writes-multipart-form
Open

fix: prevent concurrent map writes in MultiPartForm.Decode#7240
VoidChecksum wants to merge 2 commits intoprojectdiscovery:devfrom
VoidChecksum:fix/concurrent-map-writes-multipart-form

Conversation

@VoidChecksum
Copy link

@VoidChecksum VoidChecksum commented Mar 16, 2026

Summary

MultiPartForm stores mutable state (boundary string and filesMetadata map) but was registered as a singleton in dataformat.init() and returned via dataformat.Get() to every goroutine. When scanning multiple targets concurrently, this caused fatal error: concurrent map writes — a Go runtime error that cannot be caught by recover().

Root Cause

dataformat.init() → RegisterDataFormat(NewMultiPartForm())  // singleton
dataformat.Get("multipart/form-data")                       // returns same instance to all goroutines

Multiple goroutines writing to m.boundary and m.filesMetadata simultaneously triggers the fatal error.

Fix

Return a fresh MultiPartForm instance from dataformat.Get() when the requested format is multipart/form-data, since it's the only DataFormat implementation with mutable state. All other formats remain singletons (they are stateless).

func Get(name string) DataFormat {
    if name == MultiPartFormDataFormat {
        return NewMultiPartForm()
    }
    return dataformats[name]
}

Changes

  • pkg/fuzz/dataformat/dataformat.go: Return fresh instance for multipart/form-data
  • pkg/fuzz/dataformat/multipart_test.go: Add concurrent access regression test (100 goroutines)

Test plan

  • TestMultiPartFormConcurrentDecode — 100 concurrent goroutines calling Get() + Encode() without panic
  • Existing multipart tests still pass
  • go test ./pkg/fuzz/dataformat/... -race — verify no data races detected
  • Manual test: scan multiple targets with multipart/form-data fuzzing template

Fixes #7028

Summary by CodeRabbit

  • Bug Fixes

    • Improved stability for multipart form handling under concurrent requests by ensuring each operation uses an independent instance to prevent crashes.
  • Tests

    • Added concurrency tests exercising multipart form encode/decode paths to validate stability under concurrent use.

MultiPartForm stores mutable state (boundary, filesMetadata) but was
registered as a singleton shared across all goroutines. When scanning
multiple targets concurrently, this caused fatal concurrent map writes.

Return a fresh MultiPartForm instance from dataformat.Get() to ensure
each goroutine operates on its own state.

Fixes projectdiscovery#7028
Copilot AI review requested due to automatic review settings March 16, 2026 11:16
@auto-assign auto-assign bot requested a review from dogancanbakir March 16, 2026 11:16
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f3427570-0526-4d09-ace9-b9b927a1696a

📥 Commits

Reviewing files that changed from the base of the PR and between 5dcf950 and 00cc410.

📒 Files selected for processing (2)
  • pkg/fuzz/dataformat/dataformat.go
  • pkg/fuzz/dataformat/multipart_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/fuzz/dataformat/dataformat.go
  • pkg/fuzz/dataformat/multipart_test.go

Walkthrough

Get(name) now returns a fresh MultiPartForm instance when name == "multipart/form-data"; Encode/Decode use Get(...) and error if Get returns nil. Added concurrent tests to exercise Get/Decode/Encode from multiple goroutines to prevent concurrent map-write panics.

Changes

Cohort / File(s) Summary
Dataformat core change
pkg/fuzz/dataformat/dataformat.go
Get(name string) special-cases "multipart/form-data" and returns NewMultiPartForm() on each call instead of reading the shared dataformats map; Encode now obtains the encoder via Get(...) and errors if nil. Expanded doc comments on Get, Decode, and Encode (notes about MultiPartForm.IsType() behavior and singleton safety).
Concurrency regression tests
pkg/fuzz/dataformat/multipart_test.go
Added TestMultiPartFormConcurrentDecodeEncode and TestMultiPartFormConcurrentPackageLevelEncode, each spawning 100 goroutines to call Get(MultiPartFormDataFormat) and exercise ParseBoundary/Decode/Encode and the package-level Encode path; imports sync and asserts no concurrent map-write panics.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble bytes and chase the thread,
Fresh forms for each goroutine ahead,
No shared map to make me frown,
I hop through tests and guard the town,
Encoded carrots all wrapped sound. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main fix—preventing concurrent map writes in MultiPartForm.Decode by returning fresh instances instead of singletons.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can customize the tone of the review comments and chat replies.

Configure the tone_instructions setting to customize the tone of the review comments and chat replies. For example, you can set the tone to Act like a strict teacher, Act like a pirate and more.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to eliminate fatal error: concurrent map writes during concurrent multipart/form-data fuzzing by avoiding shared mutable MultiPartForm instances across goroutines.

Changes:

  • Update dataformat.Get() to return a fresh MultiPartForm instance for multipart/form-data.
  • Add a regression test intended to cover concurrent multipart access.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
pkg/fuzz/dataformat/dataformat.go Special-cases Get() to return a new MultiPartForm per call to avoid shared mutable state.
pkg/fuzz/dataformat/multipart_test.go Adds a concurrency regression test intended to prevent reintroduction of the crash.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 45 to 50
func Get(name string) DataFormat {
if name == MultiPartFormDataFormat {
return NewMultiPartForm()
}
return dataformats[name]
}
Comment on lines +373 to +376
// TestMultiPartFormConcurrentDecode verifies that Get("multipart/form-data")
// returns independent instances so concurrent goroutines do not trigger a
// fatal "concurrent map writes" panic (regression test for #7028).
func TestMultiPartFormConcurrentDecode(t *testing.T) {
Comment on lines +383 to +387
kv := mapsutil.NewOrderedMap[string, any]()
kv.Set("key", "value")
// Encode writes to the instance's filesMetadata; running this
// concurrently across separate instances must not race.
_, _ = format.Encode(KVOrderedMap(&kv))
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/fuzz/dataformat/dataformat.go (1)

96-103: ⚠️ Potential issue | 🔴 Critical

Update Encode() to use Get() for multipart/form-data to avoid concurrent map writes.

Encode() is called with "multipart/form-data" from the body decode path (body.go:91 → value.go:143, 157), but it directly accesses dataformats[dataformat] instead of using Get(). This bypasses the fresh-instance logic that prevents concurrent writes to filesMetadata, contradicting the design documented in Get()'s comments (issue #7028). The function should apply the same fresh-instance pattern for multipart/form-data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/fuzz/dataformat/dataformat.go` around lines 96 - 103, Encode currently
reads dataformats directly and bypasses the fresh-instance logic in Get(),
causing concurrent map writes for "multipart/form-data"; update Encode(data KV,
dataformat string) to call Get(dataformat) and use the returned encoder instance
(handling returned error) especially when dataformat == "multipart/form-data" so
the fresh filesMetadata instance is used, then call encoder.Encode(data) on that
instance and propagate any error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@pkg/fuzz/dataformat/dataformat.go`:
- Around line 96-103: Encode currently reads dataformats directly and bypasses
the fresh-instance logic in Get(), causing concurrent map writes for
"multipart/form-data"; update Encode(data KV, dataformat string) to call
Get(dataformat) and use the returned encoder instance (handling returned error)
especially when dataformat == "multipart/form-data" so the fresh filesMetadata
instance is used, then call encoder.Encode(data) on that instance and propagate
any error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f1501040-c2b5-4dc1-a217-fec22dc0d8ce

📥 Commits

Reviewing files that changed from the base of the PR and between b099852 and 5dcf950.

📒 Files selected for processing (2)
  • pkg/fuzz/dataformat/dataformat.go
  • pkg/fuzz/dataformat/multipart_test.go

…ency safety

Address review feedback:
- Encode() now uses Get() to obtain fresh MultiPartForm instances,
  preventing concurrent map writes through the package-level API path
- Rename TestMultiPartFormConcurrentDecode → TestMultiPartFormConcurrentDecodeEncode
  and exercise ParseBoundary + Decode + Encode with a real multipart body
- Add TestMultiPartFormConcurrentPackageLevelEncode to cover the
  package-level Encode() path
- Add note on Decode() explaining why singleton is currently safe
  (MultiPartForm.IsType() returns false)
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.

fatal error: concurrent map writes in MultiPartForm.Decode during multipart/form-data fuzzing

2 participants