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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions