Skip to content

Integer overflow in generated randFieldParams key calculation (uint32(fieldNumber<<3) overflows for fieldNumber ≥ 2^29) #778

@shinchan-op

Description

@shinchan-op

Summary

The gogo/protobuf code generator creates a test helper (randFieldParams) that computes the protobuf key as:

key := uint32(fieldNumber)<<3 | uint32(wire)

When fieldNumber >= 536,870,912 (i.e., exceeds 2^29 - 1, the maximum valid Protocol Buffers field number), the fieldNumber<<3 overflow wraps silently, resulting in invalid keys. For instance, fieldNumber = 536,870,912 << 3 = 4,294,967,296, which wraps to 0 when cast to uint32. This yields invalid protobuf field numbers (e.g., decoding yields fieldNumber = 0), corrupting test/fuzz data with high probability (~99%).

Occurrence (consumer example)

In the CometBFT repository (file api/cometbft/types/v2/params.pb.go, generated by gogo/protobuf), starting around line 1384:

func randUnrecognizedParams(r randyParams, maxFieldNumber int) (dAtA []byte) {
    l := r.Intn(5)
    for i := 0; i < l; i++ {
        wire := r.Intn(4)
        if wire == 3 {
            wire = 5
        }
        fieldNumber := maxFieldNumber + r.Intn(100)
        dAtA = randFieldParams(dAtA, r, fieldNumber, wire)
    }
    return dAtA
}

func randFieldParams(dAtA []byte, r randyParams, fieldNumber int, wire int) []byte {
    key := uint32(fieldNumber)<<3 | uint32(wire)  // ← potential overflow
    switch wire {
    // ...
    }
    return dAtA
}

Because the generator allows fieldNumber values greater than the max valid (2^29 - 1), the shift and uint32 cast overflow before varint encoding, corrupting test data.

Why This Matters

According to the Protocol Buffers spec, field numbers must be between 1 and 536,870,911. The current behavior causes invalid encoding in ~99% of test runs (r.Intn(100) ≥ 1).

Consequences:

Fuzz tests generate invalid keys that decode to fieldNumber = 0 or small unintended values.

This undermines test validity and may mask real bugs in code that handles unrecognized protobuf fields.

Proposed Solutions

Option A: Clamp fieldNumber before key computation

if fieldNumber > (1<<29 - 1) {
    fieldNumber = (1<<29 - 1)
}
key := uint32(fieldNumber)<<3 | uint32(wire)

Option B: Compute in uint64 to avoid wrap, but still enforce field number bounds:

k := (uint64(fieldNumber) << 3) | uint64(wire)
// Use `k` inside `encodeVarint...`

Still ensure fieldNumber ≤ 2^29 - 1.

Option C: Update generator template so that randUnrecognizedParams never emits invalid fieldNumber.

Reproduction Steps

Create a minimal .proto file

Save as test.proto:

syntax = "proto3";

package test;

message Dummy {
    int32 id = 1;
}

Generate Go code using gogo/protobuf

Make sure protoc is using the gogo plugin:

protoc --gogo_out=. test.proto

You should now have test.pb.go containing a function like:

func randFieldDummy(dAtA []byte, r randyDummy, fieldNumber int, wire int) []byte {
    key := uint32(fieldNumber)<<3 | uint32(wire) // <-- overflow risk
    ...
}

Write a small Go program to trigger the overflow

Save as main.go:

package main

import (
    "fmt"
)

type randyDummy interface {
    Intn(n int) int
    Int63() int64
}

type fakeRand struct{}

func (f fakeRand) Intn(n int) int {
    if n == 100 {
        return 99 // Force near-maximum fieldNumber
    }
    return 0
}
func (f fakeRand) Int63() int64 { return 0 }

func main() {
    fr := fakeRand{}
    maxFieldNumber := 536870911 // ~2^29 - 1, protobuf limit
    fieldNumber := maxFieldNumber + fr.Intn(100)
    wire := 0
    key := uint32(fieldNumber)<<3 | uint32(wire)

    fmt.Printf("Original fieldNumber: %d\n", fieldNumber)
    fmt.Printf("Shifted key (uint32 wrap): %d\n", key)
}

Run it
go run main.go

Example output:

Original fieldNumber: 536871010
Shifted key (uint32 wrap): 784

This produces invalid field numbers, breaking encoding guarantees and enabling malformed message generation during fuzz tests.

Environment

Generator: gogo/protobuf (latest)

Consumer example: CometBFT (api/cometbft/types/v2/params.pb.go)

Language: Go 1.x

OS: Cross-platform (behavior is language/runtime–independent)

Suggested Fix

I’d be happy to draft a PR if you point me to the template file responsible for generating the randUnrecognizedParams / randFieldParams logic. The fix should clamp field numbers or use uint64 for the shift during generation.

Context & Impact

This bug is critical primarily for test generation. However, it has 99% failure probability, drastically reducing code coverage and risking overlooked edge cases in real-world deployments.

Let me know the appropriate generator file or template path, and I’ll follow up with a patch, if you'd like to adapt this into a Pull Request skeleton too—I can do that next!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions