Skip to content

Small PR to test new review process [do not merge]#2136

Closed
adamdossa wants to merge 1 commit intodevelopfrom
dummy-commit
Closed

Small PR to test new review process [do not merge]#2136
adamdossa wants to merge 1 commit intodevelopfrom
dummy-commit

Conversation

@adamdossa
Copy link
Contributor

Description

Please provide a detailed description of what was done in this PR

Changes

  • Bugfix (non-breaking change that solves an issue)
  • Hotfix (change that solves an urgent issue, and requires immediate attention)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (change that is not backwards-compatible and/or changes current functionality)
  • Changes only for a subset of nodes

Breaking changes

Please complete this section if any breaking changes have been made, otherwise delete it

Nodes audience

In case this PR includes changes that must be applied only to a subset of nodes, please specify how you handled it (e.g. by adding a flag with a default value...)

Checklist

  • I have added at least 2 reviewer or the whole pos-v1 team
  • I have added sufficient documentation in code
  • I will be resolving comments - if any - by pushing each fix in a separate commit and linking the commit hash in the comment reply
  • Created a task in Jira and informed the team for implementation in Erigon client (if applicable)
  • Includes RPC methods changes, and the Notion documentation has been updated

Cross repository changes

  • This PR requires changes to heimdall
    • In case link the PR here:
  • This PR requires changes to matic-cli
    • In case link the PR here:

Testing

  • I have added unit tests
  • I have added tests to CI
  • I have tested this code manually on local environment
  • I have tested this code manually on remote devnet using express-cli
  • I have tested this code manually on amoy
  • I have created new e2e tests into express-cli

Manual tests

Please complete this section with the steps you performed if you ran manual tests for this functionality, otherwise delete it

Additional comments

Please post additional comments in this section if you have them, otherwise delete it

@sonarqubecloud
Copy link

@claude
Copy link

claude bot commented Mar 10, 2026

Code Review

Found 2 CLAUDE.md compliance issues. No bugs detected — the refactored logic is behaviorally equivalent to the original across all input cases.

Issue 1: Removed security-relevant comment (line 65)

The original comment explained why overwriting is blocked (DoS vector via arbitrary file paths). The replacement merely restates what the code does, losing the security rationale. Per CLAUDE.md commenting guidelines: DO Comment non-obvious behavior and constraints; DON'T restate code in English. Please restore the original DoS vector comment.

Issue 2: Multiple comments restate code in English (line 46)

This PR adds comments that describe what the code does rather than why, which CLAUDE.md explicitly prohibits under DON'T Comment. Affected: lines 46, 51, 68, 75, 84. Consider removing these or replacing with comments that explain why.

Comment on lines 66 to 74
}
// 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()

Copy link

Choose a reason for hiding this comment

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

🟣 Pre-existing: ExportChain has a TOCTOU race between os.Stat (line 66) and os.OpenFile with O_CREATE|O_TRUNC (line 71) -- another process could create the file in between, and O_TRUNC would 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 and O_TRUNC with os.O_EXCL (atomic create-or-fail), and add a deferred os.Remove on 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 with os.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. Because O_TRUNC is used, OpenFile would 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 file parameter comes from an RPC caller and can point to arbitrary filesystem paths.

Missing Error Cleanup

If ExportN or Export fails after os.OpenFile has already created the file, the function returns (false, error) but never deletes the partially-written file. On a retry attempt, the os.Stat check 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:

  1. User calls ExportChain("dump.gz", first, last)
  2. os.Stat("dump.gz") returns an error (file does not exist) -- check passes
  3. os.OpenFile creates dump.gz successfully
  4. ExportN fails midway (e.g., a block is missing from the chain)
  5. Function returns (false, exportErr) -- defer out.Close() runs but no os.Remove
  6. dump.gz remains on disk as a partial/corrupt file
  7. User retries ExportChain("dump.gz", first, last)
  8. os.Stat("dump.gz") succeeds (file exists from step 3) -> returns "location would overwrite an existing file"
  9. Export is permanently stuck without manual file deletion

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.Stat check and O_TRUNC with os.O_EXCL, which atomically fails if the file already exists:

out, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)

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:

defer func() {
    out.Close()
    if exportErr \!= nil {
        os.Remove(file)
    }
}()

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.

Comment on lines +80 to 83
writer = gzWriter
defer gzWriter.Close()
}

Copy link

Choose a reason for hiding this comment

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

🟣 Pre-existing issue: defer gzWriter.Close() discards the error from gzip.Writer.Close(), which flushes remaining compressed data and writes the GZIP footer. If this flush fails (e.g., disk full), ExportChain returns (true, nil) indicating success while the .gz file is actually corrupt/truncated. Consider closing the gzip writer explicitly before the success return and checking its error.

Extended reasoning...

What the bug is

gzip.Writer.Close() is not just a resource cleanup call — it flushes any remaining compressed data from internal buffers and writes the mandatory GZIP footer (CRC32 checksum and size fields). If this operation fails (due to disk full, I/O error, etc.), the error is silently discarded by defer gzWriter.Close() on line 83. The function then proceeds to return (true, nil) on line 93, telling the caller the export succeeded.

Step-by-step proof

  1. User calls ExportChain("chain.gz", nil, nil)
  2. File is created, gzWriter is initialized (line 82), and defer gzWriter.Close() is registered (line 83)
  3. api.eth.BlockChain().Export(writer) succeeds — exportErr is nil
  4. The function reaches line 93: return true, nil — caller is told export succeeded
  5. Deferred gzWriter.Close() runs, attempting to flush remaining compressed data and write the GZIP footer
  6. The write fails (e.g., disk became full during the deferred close)
  7. The error from Close() is discarded — nobody checks it
  8. The .gz file on disk is missing its GZIP footer, making it corrupt — gunzip will report an error

Why existing code doesn't prevent it

The defer statement on line 83 calls gzWriter.Close() but does not capture or propagate the returned error. This is a common Go pattern for read cleanup (where close errors are usually benign), but for write operations — especially gzip, which performs meaningful work on Close — the error is significant.

Pre-existing nature

The old code had the identical issue: defer writer.(*gzip.Writer).Close() also discarded the error. This PR refactored the gzip handling (introducing the named gzWriter variable), which was an opportunity to fix this, but the error-ignoring behavior was carried over unchanged. The pattern also appears in other places in the codebase (e.g., cmd/utils/cmd.go).

How to fix

Close 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, nil

The defer gzWriter.Close() can be kept as a safety net (it is a no-op if already closed), or removed entirely since the explicit close covers the success path and the error paths don't need a valid gzip footer anyway.

@adamdossa adamdossa closed this Mar 11, 2026
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.

1 participant