Skip to content

Data race when accessing dirties map in hashdb.Database.commit #28541

Closed
@magicxyyz

Description

@magicxyyz

System information

Commit hash: c8a2202 (current master)

Issue description

In trie/triedb/hashdb.Database.commit a key is read from dirites map without locking db lock, while trie/tiredb/hashdb.Database.dereference may modify the map concurrently resulting in concurrent map read and map write panic.

Possible scenario affected by the issue

One scenario affected by the race is when a new block state is committed and concurrently an rpc is called (e.g. debug_traceBlockByNumber) which executes eth.StateAtBlock querying a state that is available in live database and then executes tracers.StateReleaseFunc which dereferences a node.

Code pointers

  • Read access to dirties map:

    func (db *Database) commit(hash common.Hash, batch ethdb.Batch, uncacher *cleaner) error {
    // If the node does not exist, it's a previously committed node
    node, ok := db.dirties[hash]
    if !ok {
    return nil
    }

  • Write access to dirties map:

    delete(db.dirties, hash)

  • Example of place where dereference is called in rpc handling goroutine:

func (api *API) traceBlock(ctx context.Context, block *types.Block, config *TraceConfig) ([]*txTraceResult, error) {
if block.NumberU64() == 0 {
return nil, errors.New("genesis is not traceable")
}
// Prepare base state
parent, err := api.blockByNumberAndHash(ctx, rpc.BlockNumber(block.NumberU64()-1), block.ParentHash())
if err != nil {
return nil, err
}
reexec := defaultTraceReexec
if config != nil && config.Reexec != nil {
reexec = *config.Reexec
}
statedb, release, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false)
if err != nil {
return nil, err
}
defer release()

func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, tracers.StateReleaseFunc, error) {
return b.eth.stateAtBlock(ctx, block, reexec, base, readOnly, preferDisk)

func (eth *Ethereum) stateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (statedb *state.StateDB, release tracers.StateReleaseFunc, err error) {
if eth.blockchain.TrieDB().Scheme() == rawdb.HashScheme {
return eth.hashState(ctx, block, reexec, base, readOnly, preferDisk)
}

if readOnly {
// The state is available in live database, create a reference
// on top to prevent garbage collection and return a release
// function to deref it.
if statedb, err = eth.blockchain.StateAt(block.Root()); err == nil {
eth.blockchain.TrieDB().Reference(block.Root(), common.Hash{})
return statedb, func() {
eth.blockchain.TrieDB().Dereference(block.Root())
}, nil
}
}

Proposed solution

Lock db lock when accessing dirties map in hashdb.Database.commit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions