-
Notifications
You must be signed in to change notification settings - Fork 582
Small PR to test new review process [do not merge] #2136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,39 +42,54 @@ func NewAdminAPI(eth *Ethereum) *AdminAPI { | |
|
|
||
| // ExportChain exports the current blockchain into a local file, | ||
| // or a range of blocks if first and last are non-nil. | ||
| func (api *AdminAPI) ExportChain(file string, first *uint64, last *uint64) (bool, error) { | ||
| func (api *AdminAPI) ExportChain(file string, first, last *uint64) (bool, error) { | ||
| // Validate input: last cannot be specified without first | ||
| if first == nil && last != nil { | ||
| return false, errors.New("last cannot be specified without first") | ||
| } | ||
| if first != nil && last == nil { | ||
| head := api.eth.BlockChain().CurrentHeader().Number.Uint64() | ||
| last = &head | ||
|
|
||
| // If first is provided but last isn't, use current chain head as last | ||
| var exportFirst, exportLast *uint64 | ||
| if first != nil { | ||
| exportFirst = first | ||
| if last != nil { | ||
| exportLast = last | ||
| } else { | ||
| head := api.eth.BlockChain().CurrentHeader().Number.Uint64() | ||
| exportLast = &head | ||
| } | ||
| } | ||
|
|
||
| // Prevent overwriting an existing file | ||
| if _, err := os.Stat(file); err == nil { | ||
| // File already exists. Allowing overwrite could be a DoS vector, | ||
| // since the 'file' may point to arbitrary paths on the drive. | ||
| return false, errors.New("location would overwrite an existing file") | ||
| } | ||
| // Make sure we can create the file to export into | ||
|
|
||
| // Defer cleanup and resource closure | ||
| out, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| defer out.Close() | ||
|
|
||
| var writer io.Writer = out | ||
| // Handle gzip compression transparently | ||
| writer := io.Writer(out) | ||
| var gzWriter *gzip.Writer | ||
| if strings.HasSuffix(file, ".gz") { | ||
| writer = gzip.NewWriter(writer) | ||
| defer writer.(*gzip.Writer).Close() | ||
| gzWriter = gzip.NewWriter(out) | ||
| writer = gzWriter | ||
| defer gzWriter.Close() | ||
| } | ||
|
|
||
|
Comment on lines
+80
to
83
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟣 Pre-existing issue: Extended reasoning...What the bug is
Step-by-step proof
Why existing code doesn't prevent itThe Pre-existing natureThe old code had the identical issue: How to fixClose the gzip writer explicitly before the success return and check its error: if exportErr != nil {
return false, exportErr
}
if gzWriter != nil {
if err := gzWriter.Close(); err != nil {
return false, err
}
}
return true, nilThe |
||
| // Export the blockchain | ||
| if first != nil { | ||
| if err := api.eth.BlockChain().ExportN(writer, *first, *last); err != nil { | ||
| return false, err | ||
| } | ||
| } else if err := api.eth.BlockChain().Export(writer); err != nil { | ||
| return false, err | ||
| // Export blockchain data | ||
| var exportErr error | ||
| if exportFirst != nil { | ||
| exportErr = api.eth.BlockChain().ExportN(writer, *exportFirst, *exportLast) | ||
| } else { | ||
| exportErr = api.eth.BlockChain().Export(writer) | ||
| } | ||
| if exportErr != nil { | ||
| return false, exportErr | ||
| } | ||
| return true, nil | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟣 Pre-existing:
ExportChainhas a TOCTOU race betweenos.Stat(line 66) andos.OpenFilewithO_CREATE|O_TRUNC(line 71) -- another process could create the file in between, andO_TRUNCwould silently overwrite it. Additionally, if the export fails after the file is created, the stale file is never cleaned up, making retries permanently fail with "location would overwrite an existing file". Both issues existed before this PR, but the refactoring touched this exact code and could have fixed them. Replace the Stat check andO_TRUNCwithos.O_EXCL(atomic create-or-fail), and add a deferredos.Removeon error.Extended reasoning...
TOCTOU Race Condition
The code at lines 66-71 first checks whether a file exists using
os.Stat(file), then opens it withos.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644). Between these two calls, another process could create a file at the same path. BecauseO_TRUNCis used,OpenFilewould silently overwrite the newly-created file rather than failing. This is the classic Time-of-Check-to-Time-of-Use (TOCTOU) pattern.The old code actually had a comment warning about this exact DoS vector: "File already exists. Allowing overwrite could be a DoS vector, since the file may point to arbitrary paths on the drive." This PR removed that comment during refactoring, reducing future developer awareness of the risk. The
fileparameter comes from an RPC caller and can point to arbitrary filesystem paths.Missing Error Cleanup
If
ExportNorExportfails afteros.OpenFilehas already created the file, the function returns(false, error)but never deletes the partially-written file. On a retry attempt, theos.Statcheck finds the leftover file and returns "location would overwrite an existing file", permanently blocking retries until the user manually deletes the file.Step-by-step proof:
ExportChain("dump.gz", first, last)os.Stat("dump.gz")returns an error (file does not exist) -- check passesos.OpenFilecreatesdump.gzsuccessfullyExportNfails midway (e.g., a block is missing from the chain)(false, exportErr)--defer out.Close()runs but noos.Removedump.gzremains on disk as a partial/corrupt fileExportChain("dump.gz", first, last)os.Stat("dump.gz")succeeds (file exists from step 3) -> returns "location would overwrite an existing file"The comment added at line 70 -- "Defer cleanup and resource closure" -- is misleading because no actual file cleanup (deletion) occurs on error; only the file handle is closed.
Recommended Fix
Replace both the
os.Statcheck andO_TRUNCwithos.O_EXCL, which atomically fails if the file already exists:This eliminates the race condition and the need for the separate Stat check entirely. Additionally, add a deferred cleanup to remove the file on export error:
Both issues are pre-existing (the old code had the identical patterns), but since this PR directly refactors the affected code, it is a natural opportunity to address them.