-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Conversation
5ba0e80
to
88e53e5
Compare
88e53e5
to
378a971
Compare
…security warnings Fixes errors reported by cosmos/gosec for non-deterministic map ranging, but using golang.org/x/exp/maps.(Keys, Values) where necessary.
378a971
to
f28f5f4
Compare
for key := range rs.removalMap { | ||
keys = append(keys, key) | ||
} |
Check warning
Code scanning / CodeQL
Iteration over map
for key := range rs.stores { | ||
keys = append(keys, key) | ||
} |
Check warning
Code scanning / CodeQL
Iteration over map
There was a problem hiding this 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
) 🙏 ?
There was a problem hiding this 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.
keys := maps.Keys(queueItems) | ||
sort.Strings(keys) | ||
for _, key := range keys { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
// Ensure that downloads proceed in a deterministic pattern. | ||
osArches := maps.Keys(m) | ||
sort.Strings(osArches) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likewise above.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
// Ensure that downloads proceed in a deterministic pattern. | ||
osArches := maps.Keys(m) | ||
sort.Strings(osArches) | ||
for _, osArch := range osArches { | ||
url := m[osArch] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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 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 |
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 |
Re-opening as there is discussion here. |
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 :-)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does.
There was a problem hiding this comment.
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.
names := maps.Keys(commandOptions.FlagOptions) | ||
for _, name := range names { |
There was a problem hiding this comment.
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?
// Ensure that building module query commands is in a deterministic pattern. | ||
moduleNames := maps.Keys(moduleOptions) | ||
sort.Strings(moduleNames) | ||
for _, moduleName := range moduleNames { |
There was a problem hiding this comment.
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?
// Ensure that downloads proceed in a deterministic pattern. | ||
osArches := maps.Keys(m) | ||
sort.Strings(osArches) |
There was a problem hiding this comment.
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?
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] |
There was a problem hiding this comment.
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?
for _, val := range currValidators { | ||
valL := maps.Values(currValidators) | ||
for _, val := range valL { | ||
if len(val.Vote) == 0 { | ||
continue | ||
} |
There was a problem hiding this comment.
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?
func (t KeyTable) maxKeyLength() (res int) { | ||
for k := range t.m { | ||
for _, k := range maps.Keys(t.m) { | ||
l := len(k) | ||
if l > res { |
There was a problem hiding this comment.
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?
for valAddrStr := range last { | ||
valAddrL := maps.Keys(last) | ||
for _, valAddrStr := range valAddrL { |
There was a problem hiding this comment.
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?
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] |
There was a problem hiding this comment.
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?
Checking in on status of this pr? Should we take it over? |
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 |
Fixes errors reported by cosmos/gosec for non-deterministic map ranging, but using golang.org/x/exp/maps.(Keys, Values) where necessary.