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

fix: all: avoid map ranging whenever possible to fix non-determinism security warnings #13554

Closed

Conversation

odeke-em
Copy link
Collaborator

Fixes errors reported by cosmos/gosec for non-deterministic map ranging, but using golang.org/x/exp/maps.(Keys, Values) where necessary.

@odeke-em odeke-em force-pushed the gosec-fix-map-iteration-non-determinism-warnings branch 2 times, most recently from 5ba0e80 to 88e53e5 Compare October 15, 2022 04:14
@odeke-em odeke-em changed the title all: avoid map ranging whenever possible to fix non-determinism security warnings fix: all: avoid map ranging whenever possible to fix non-determinism security warnings Oct 15, 2022
@odeke-em odeke-em force-pushed the gosec-fix-map-iteration-non-determinism-warnings branch from 88e53e5 to 378a971 Compare October 15, 2022 04:15
…security warnings

Fixes errors reported by cosmos/gosec for non-deterministic map
ranging, but using golang.org/x/exp/maps.(Keys, Values) where
necessary.
@odeke-em odeke-em force-pushed the gosec-fix-map-iteration-non-determinism-warnings branch from 378a971 to f28f5f4 Compare October 15, 2022 04:31
Comment on lines +443 to +445
for key := range rs.removalMap {
keys = append(keys, key)
}

Check warning

Code scanning / CodeQL

Iteration over map

Iteration over map may be a possible source of non-determinism
Comment on lines +594 to +596
for key := range rs.stores {
keys = append(keys, key)
}

Check warning

Code scanning / CodeQL

Iteration over map

Iteration over map may be a possible source of non-determinism
Copy link
Contributor

@alexanderbez alexanderbez left a comment

Choose a reason for hiding this comment

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

Can we be consistent with using maps.Keys inline with the range rather than creating a new variable (unless necessary, i.e. keys used more than range) 🙏 ?

Copy link

@peterbourgon peterbourgon left a comment

Choose a reason for hiding this comment

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

Go maps are a core language primitive, used in almost every Go program. They are also defined by the language spec to have a non-deterministic iteration order. I don't think it's reasonable to assert that the entirely normal, idiomatic, and ubiquitous expression for k, v := range m is somehow a problem. All Go programs are going to do this.

If a specific bit of code needs range determinism, that's fine! But that code needs to ensure that invariant explicitly. It doesn't seem correct to refactor all map range exprs in the whole project — extract the keys, sort them, iterate them, and then look up the corresponding val in the map — just by default. Determinism is the exception, not the rule.

Comment on lines +65 to +67
keys := maps.Keys(queueItems)
sort.Strings(keys)
for _, key := range keys {

Choose a reason for hiding this comment

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

There should be no reason to sort the return value of maps.Keys.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There is an absolute reason to, maps.Keys returns them in non-deterministic order and the source code shows it https://cs.opensource.google/go/x/exp/+/32f3d567:maps/maps.go;l=10
Screen Shot 2022-10-28 at 9 16 39 PM

Choose a reason for hiding this comment

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

Surprising! But if map.Keys doesn't sort the keys, then the dependency provides very little value, right? It would be better to define your own package map with a Keys that returned ordered keys, I guess?

Comment on lines +108 to +110
// Ensure that downloads proceed in a deterministic pattern.
osArches := maps.Keys(m)
sort.Strings(osArches)

Choose a reason for hiding this comment

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

Likewise above.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nope, we need these deterministic.

Choose a reason for hiding this comment

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

Is that requirement documented somewhere?

Comment on lines +108 to +112
// Ensure that downloads proceed in a deterministic pattern.
osArches := maps.Keys(m)
sort.Strings(osArches)
for _, osArch := range osArches {
url := m[osArch]

Choose a reason for hiding this comment

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

Suggested change
// Ensure that downloads proceed in a deterministic pattern.
osArches := maps.Keys(m)
sort.Strings(osArches)
for _, osArch := range osArches {
url := m[osArch]
for _, k := range maps.Keys(m) {
url := m[k]

@tac0turtle tac0turtle closed this Oct 28, 2022
@odeke-em
Copy link
Collaborator Author

@tac0turtle how come you closed this change? @peterbourgon provided feedback but it wasn't entirely correct about maps.Keys not needing sorting as we need determinism per https://cs.opensource.google/go/x/exp/+/32f3d567:maps/maps.go;l=10
Screen Shot 2022-10-28 at 9 16 39 PM
which as you can see just iterates over keys. We want those values sorted.

@odeke-em
Copy link
Collaborator Author

Go maps are a core language primitive, used in almost every Go program. They are also defined by the language spec to have a non-deterministic iteration order. I don't think it's reasonable to assert that the entirely normal, idiomatic, and ubiquitous expression for k, v := range m is somehow a problem. All Go programs are going to do this.

If a specific bit of code needs range determinism, that's fine! But that code needs to ensure that invariant explicitly. It doesn't seem correct to refactor all map range exprs in the whole project — extract the keys, sort them, iterate them, and then look up the corresponding val in the map — just by default. Determinism is the exception, not the rule.

Thanks @peterbourgon for the initial review! So parts of this code does need determinism, please read the PR much closer and look at the directories, there are 5 usages of sort. out of the 19 files and that code is in super sensitive order code like store/v* for which writes to databases should have a deterministic ordering. Let's reopen this @tac0turtle

@julienrbrt
Copy link
Member

Re-opening as there is discussion here.

@julienrbrt julienrbrt reopened this Oct 29, 2022
Copy link

@peterbourgon peterbourgon left a comment

Choose a reason for hiding this comment

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

If maps.Keys and maps.Values don't sort their predicates, then I guess there are some problems here? I might be misunderstanding something!

@@ -230,7 +230,7 @@ func (app *BaseApp) MountStores(keys ...storetypes.StoreKey) {
// MountKVStores mounts all IAVL or DB stores to the provided keys in the
// BaseApp multistore.
func (app *BaseApp) MountKVStores(keys map[string]*storetypes.KVStoreKey) {
for _, key := range keys {
for _, key := range maps.Values(keys) {

Choose a reason for hiding this comment

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

Values returns the values of the map m. The values will be in an indeterminate order.

Does this change solve a problem?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For some code here such as this one, the values can be returned non-deterministically. The critical ones, I invoke sort.* on them :-)

Copy link
Member

Choose a reason for hiding this comment

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

naive question, why is map.Values and map.Keys needed here? does it add anything the previous code didnt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure: for code that's okay with being non-deterministic, they mask the fact that we are ranging over maps given that the static analyzer rule is rigid. We could add a comment that it is okay to range over them but that'll pollute lots of code so just easier to use maps.Values and maps.Key

Copy link
Member

Choose a reason for hiding this comment

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

I still don't understand sorry. it seems like this adds an extra loop of the map to grab the keys or values. It seems this fixes the linter but doesn't actually fix any logic for us. this doesn't seem to sort like the title suggests.

Id prefer not to add things like this if its not fixing an issue we are facing.

Choose a reason for hiding this comment

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

FWIW, that static analyzer is bogus and should be removed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@peterbourgon that's quite strong language most definitely from lack of context/ignorance of the history, but the reaasoning is there was a serious chain halt that happened late last year due to non-deterministic map ranging https://twitter.com/thorchain/status/1459288646142492673 and https://twitter.com/thorchain/status/1459288646142492673 and the cosmos-sdk and other projects have been plagued by such bugs due to non-deterministic map ranging.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@tac0turtle yeah it requires a balance but sadly would require annotations or perhaps just scoped to specific packages, but we need to figure things out. Also this change is a Draft btw folks, not yet ready for review.

Copy link
Member

Choose a reason for hiding this comment

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

The bugs on chains aren't from map ranging but from non-deterministic ordering across nodes in the network, which i dont see how this fixes that issue. These functions are loops with appends of keys or values to an array, not sorting. We can stop commenting but this feels unnecessary

Copy link

@peterbourgon peterbourgon Nov 2, 2022

Choose a reason for hiding this comment

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

@peterbourgon that's quite strong language most definitely from lack of context/ignorance of the history, but the reaasoning is there was a serious chain halt that happened late last year due to non-deterministic map ranging https://twitter.com/thorchain/status/1459288646142492673 and https://twitter.com/thorchain/status/1459288646142492673 and the cosmos-sdk and other projects have been plagued by such bugs due to non-deterministic map ranging.

I'm aware of the chain halt issues you're referring to. I was directly affected by them! 😅 The problem was not that range over a map is non-deterministic. The problem was the assumption, made by the programmer who wrote the code, that ranging over a map would be — or could be! — deterministic. Any code that needs deterministic iteration order over a map-like collection can't use the builtin map type directly. It either has to extract and sort the keys manually, or use a different type that provides this property.

And it's totally fine to extract the keys from a map, sort them, and iterate the map based on that slice, on an as-needed basis. Perfect, no problem. But this is a special case, it's not the baseline. That ranging over a map is nondeterministic is simply a property of the language. If you write Go, you need to understand that property, same as anything else, like that that switch statements don't fallthrough by default, or that you can read from a zero-value map but not write to it, or etc. etc. These things aren't problems that need to be — or even can be! — corrected by linters. They're the rules of the game.

@@ -244,7 +244,8 @@ func (app *BaseApp) MountKVStores(keys map[string]*storetypes.KVStoreKey) {
// MountTransientStores mounts all transient stores to the provided keys in
// the BaseApp multistore.
func (app *BaseApp) MountTransientStores(keys map[string]*storetypes.TransientStoreKey) {
for _, key := range keys {
keyL := maps.Values(keys)

Choose a reason for hiding this comment

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

Values returns the values of the map m. The values will be in an indeterminate order.

Does this change solve a problem?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It does.

Choose a reason for hiding this comment

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

What problem does it solve? Is it to silence the linter warnings about nondeterinistic iteration order? This change does seem to silence the linter, but it doesn't change the behavior, right? The order is still nondeterministic.

Comment on lines +71 to +72
names := maps.Keys(commandOptions.FlagOptions)
for _, name := range names {

Choose a reason for hiding this comment

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

Keys returns the keys of the map m. The keys will be in an indeterminate order.

Does this change solve a problem?

Comment on lines +25 to +28
// Ensure that building module query commands is in a deterministic pattern.
moduleNames := maps.Keys(moduleOptions)
sort.Strings(moduleNames)
for _, moduleName := range moduleNames {

Choose a reason for hiding this comment

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

Is it documented somewhere that BuildQueryCommand should iterate over module names deterministically? If not, is it necessary to do so? If so, is there a test which enforces this invariant? And isn't manual sorting of keys somewhat error prone? Should there rather be a package/function that ensures deterministic order without requiring explicit sorting at the callsite?

Comment on lines +108 to +110
// Ensure that downloads proceed in a deterministic pattern.
osArches := maps.Keys(m)
sort.Strings(osArches)

Choose a reason for hiding this comment

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

Is that requirement documented somewhere?

Comment on lines -589 to +598
for key, store := range rs.stores {
keys := make([]types.StoreKey, 0, len(rs.stores))
for key := range rs.stores {
keys = append(keys, key)
}
for _, key := range keys {
store := rs.stores[key]

Choose a reason for hiding this comment

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

This code isn't sorting the keys, so AFAICT doesn't produce behavior any different than ranging over the map directly?

Comment on lines -76 to 81
for _, val := range currValidators {
valL := maps.Values(currValidators)
for _, val := range valL {
if len(val.Vote) == 0 {
continue
}

Choose a reason for hiding this comment

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

This code isn't sorting the values, so AFAICT doesn't produce behavior any different than ranging over the map directly?

Comment on lines 76 to 79
func (t KeyTable) maxKeyLength() (res int) {
for k := range t.m {
for _, k := range maps.Keys(t.m) {
l := len(k)
if l > res {

Choose a reason for hiding this comment

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

This code isn't sorting the keys, so AFAICT doesn't produce behavior any different than ranging over the map directly?

Comment on lines -402 to +404
for valAddrStr := range last {
valAddrL := maps.Keys(last)
for _, valAddrStr := range valAddrL {

Choose a reason for hiding this comment

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

This code isn't sorting the keys, so AFAICT doesn't produce behavior any different than ranging over the map directly?

Comment on lines -80 to +88
for key, val := range m {
for _, key := range maps.Keys(m) {
if key != "any" && !osArchRx.MatchString(key) {
return fmt.Errorf("invalid os/arch format in key \"%s\"", key)
}

val := m[key]

Choose a reason for hiding this comment

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

This code isn't sorting the keys, so AFAICT doesn't produce behavior any different than ranging over the map directly?

@tac0turtle
Copy link
Member

Checking in on status of this pr? Should we take it over?

@tac0turtle
Copy link
Member

Hey the core sdk Team discussed closing prs which have been inactive for 30 days.

I'll close this for now. Let us know if you need help with any review in the future

@tac0turtle tac0turtle closed this Dec 2, 2022
@tac0turtle tac0turtle deleted the gosec-fix-map-iteration-non-determinism-warnings branch April 14, 2023 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants