Skip to content
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

Add static analysis for unsafe uint casting #10318

Merged
merged 37 commits into from
Mar 11, 2022
Merged

Conversation

prestonvanloon
Copy link
Member

@prestonvanloon prestonvanloon commented Mar 7, 2022

What type of PR is this?

Other

What does this PR do? Why is it needed?

Casting uint types to int types can lead to unexpected results, i.e. overflows.

This PR adds a static analysis check for these scenarios and resolves all found issues.

Which issues(s) does this PR fix?

Other notes for review

One thing I thought about is that it's technically not a big deal to cast a smaller uint to a larger int (uint32 -> int64), but we typically only deal with uint64 so I decided to warn on any conversion from uint to int.

@rauljordan
Copy link
Contributor

Note this closes #9958 if all exceptions are taken care of

// Defensive check to prevent potential issues when casting to int64.
if length > math.MaxInt64 {
return 0, errors.Errorf("invalid length provided: %d", length)
il, err := math.Int(length)
Copy link
Member Author

Choose a reason for hiding this comment

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

This validation has moved to math.Int

@@ -2,6 +2,7 @@ load("@prysm//tools/go:def.bzl", "go_library")

go_library(
name = "go_default_library",
testonly = True,
Copy link
Member Author

Choose a reason for hiding this comment

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

Found this in my journey. More test code not marked as testonly :(

@prestonvanloon prestonvanloon marked this pull request as ready for review March 7, 2022 22:18
@prestonvanloon prestonvanloon requested review from rkapka, nisdas and a team as code owners March 7, 2022 22:18
@prestonvanloon prestonvanloon requested a review from kasey March 7, 2022 22:18
@prestonvanloon prestonvanloon added OK to merge Security Security Related Issues labels Mar 7, 2022
@prestonvanloon prestonvanloon linked an issue Mar 7, 2022 that may be closed by this pull request
2 tasks
@prestonvanloon prestonvanloon requested a review from uncdr March 7, 2022 23:31
Copy link
Contributor

@rauljordan rauljordan left a comment

Choose a reason for hiding this comment

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

Found a test case the analyzer does not catch: when doing the cast during struct initialization:

func Uint64CastInStruct() {
	type S struct {
		a int
	}
	s := S{
		a: int(uint64(5)), // want "Unsafe cast from uint64 to int."
	}
	_ = s
}

this fails when running the Analyzer test

WORKSPACE Outdated Show resolved Hide resolved
beacon-chain/forkchoice/protoarray/helpers.go Show resolved Hide resolved
tools/analyzers/uintcast/analyzer_test.go Show resolved Hide resolved
// maxBalanceCacheSize defines the max number of active balances can cache.
maxBalanceCacheSize = uint64(4)
maxBalanceCacheSize = int(4)
Copy link
Member

Choose a reason for hiding this comment

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

This was brought up offline cc @uncdr , but is there any reason we have cache sizes as a signed integer here ? It can't be negative in any case.

Copy link
Member

Choose a reason for hiding this comment

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

Ah k, I see it is due to the lruwrpr, maybe we can change the method signature to take a uint64. And it does the safe casting internally in the lru constructor.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah exactly, I moved these to const so that we are assured it can never change. I thought this was the easier route vs changing 3rd party API

Copy link
Member

@nisdas nisdas Mar 8, 2022

Choose a reason for hiding this comment

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

It is not 3rd party though, this lruwrpr package is under prysm.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh! OK, I'll take a look!

Copy link
Member Author

Choose a reason for hiding this comment

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

Why do we even have lruwrpr? It's a flow through to github.com/hashicorp/golang-lru with panics. I'm going to leave it out of scope for this PR. It seems strange to have this package when we could use lru directly.

Copy link
Member

Choose a reason for hiding this comment

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

I think we wanted the lru package to not return an error mostly. The package returns an error if the cache size is negative.

Copy link
Member

Choose a reason for hiding this comment

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

For this, it might be best to handle this in a follow up PR. The LRU cache is used across prysm and changing the api of it, will require changes across prysm

@prestonvanloon
Copy link
Member Author

Found a test case the analyzer does not catch: when doing the cast during struct initialization:

func Uint64CastInStruct() {
	type S struct {
		a int
	}
	s := S{
		a: int(uint64(5)), // want "Unsafe cast from uint64 to int."
	}
	_ = s
}

this fails when running the Analyzer test

This is a pretty contrived example. I will see if I can reasonably add to the SA to solve this, but I might not be able to do it in this PR

Copy link

@uncdr uncdr left a comment

Choose a reason for hiding this comment

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

i'm still not ok where int, uint, uint64 should be used

// maxCommitteesCacheSize defines the max number of shuffled committees on per randao basis can cache.
// Due to reorgs and long finality, it's good to keep the old cache around for quickly switch over.
maxCommitteesCacheSize = uint64(32)
maxCommitteesCacheSize = int(32)
Copy link

Choose a reason for hiding this comment

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

same as in another thread, why signed int?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's the 3rd party API. I'm not sure. This is a constant now so it can't overflow/underflow. I think we can address cache API in another PR or issue.

@@ -92,7 +92,7 @@ func postAltairMsgID(pmsg *pubsub_pb.Message, fEpoch types.Epoch) string {
h := hash.Hash(combinedData)
return string(h[:20])
}
totalLength := len(params.BeaconNetworkConfig().MessageDomainValidSnappy) + len(topicLenBytes) + int(topicLen) + len(decodedData)
totalLength := len(params.BeaconNetworkConfig().MessageDomainValidSnappy) + len(topicLenBytes) + int(topicLen) + len(decodedData) // lint:ignore uintcast -- Message length cannot exceed int64
Copy link

Choose a reason for hiding this comment

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

if totalLength exceeds int64? overflow and a negative value?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll add a SafeAddInt64 method for this

@prestonvanloon
Copy link
Member Author

i'm still not ok where int, uint, uint64 should be used

int and uint are now guaranteed to be 64 bits. Go automatically sets int and uint to the max size available to the system and I have added a runtime check to panic on start if the max size of int/uint is not 64 bits.

In general, int should be used to interact with an API that we do not control.
For example, indexing an array/slice requires an int.

arr[i] // i must be int

There are a few other use cases where int is allowed when negative numbers are expected, like balance deltas in fork choice.

In most cases, uint64 is the canonical type for numbers in Ethereum.

@prestonvanloon
Copy link
Member Author

Removing OK to merge until after someone approves this. Since this PR adds new static analysis, there is no cache hits between develop based branches and this one. So this PR is contributing to high build agent disk usage until it can be merged.

@prylabs-bulldozer prylabs-bulldozer bot merged commit c1197d7 into develop Mar 11, 2022
@delete-merged-branch delete-merged-branch bot deleted the uintcast-sa branch March 11, 2022 09:34
michaelneuder pushed a commit to michaelneuder/prysm that referenced this pull request Mar 11, 2022
* Add static analysis for unsafe uint casting

* Fix violations of uintcast

* go mod tidy

* Add exclusion to nogo for darwin build

* Add test for math.Int

* Move some things to const so they are assured not to exceed int64

* Self review

* lint

* fix tests

* fix test

* Add init check for non 64 bit OS

* Move new deps from WORKSPACE to deps.bzl

* fix bazel build for go analysis runs

* Update BUILD.bazel

Remove TODO

* add math.AddInt method

* Add new test casts

* Add case where builtin functions and declared functions are covered

* Fix new findings

* cleanup

Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
Co-authored-by: Nishant Das <nishdas93@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Security Security Related Issues
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Remove Unnecessary Use of Integer Types in Prysm
4 participants