Skip to content
This repository has been archived by the owner on Jul 22, 2024. It is now read-only.

Commit

Permalink
Merge pull request #20 from aaronlehmann/fix-set-hashing
Browse files Browse the repository at this point in the history
Make set hashing robust to bitwise XOR cancellations
  • Loading branch information
mitchellh authored Nov 22, 2020
2 parents 191a1e8 + efa4282 commit d5de409
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 2 deletions.
34 changes: 34 additions & 0 deletions hashstructure.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
h = hashUpdateUnordered(h, fieldHash)
}

h = hashFinishUnordered(w.h, h)

return h, nil

case reflect.Struct:
Expand Down Expand Up @@ -350,6 +352,8 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
fieldHash := hashUpdateOrdered(w.h, kh, vh)
h = hashUpdateUnordered(h, fieldHash)
}

h = hashFinishUnordered(w.h, h)
}

return h, nil
Expand Down Expand Up @@ -377,6 +381,10 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
}
}

if set {
h = hashFinishUnordered(w.h, h)
}

return h, nil

case reflect.String:
Expand Down Expand Up @@ -413,6 +421,32 @@ func hashUpdateUnordered(a, b uint64) uint64 {
return a ^ b
}

// After mixing a group of unique hashes with hashUpdateUnordered, it's always
// necessary to call hashFinishUnordered. Why? Because hashUpdateUnordered
// is a simple XOR, and calling hashUpdateUnordered on hashes produced by
// hashUpdateUnordered can effectively cancel out a previous change to the hash
// result if the same hash value appears later on. For example, consider:
//
// hashUpdateUnordered(hashUpdateUnordered("A", "B"), hashUpdateUnordered("A", "C")) =
// H("A") ^ H("B")) ^ (H("A") ^ H("C")) =
// (H("A") ^ H("A")) ^ (H("B") ^ H(C)) =
// H(B) ^ H(C) =
// hashUpdateUnordered(hashUpdateUnordered("Z", "B"), hashUpdateUnordered("Z", "C"))
//
// hashFinishUnordered "hardens" the result, so that encountering partially
// overlapping input data later on in a different context won't cancel out.
func hashFinishUnordered(h hash.Hash64, a uint64) uint64 {
h.Reset()

// We just panic if the writes fail
e1 := binary.Write(h, binary.LittleEndian, a)
if e1 != nil {
panic(e1)
}

return h.Sum64()
}

// visitFlag is used as a bitmask for affecting visit behavior
type visitFlag uint

Expand Down
2 changes: 1 addition & 1 deletion hashstructure_examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ func ExampleHash() {

fmt.Printf("%d", hash)
// Output:
// 6691276962590150517
// 1839806922502695369
}
2 changes: 1 addition & 1 deletion hashstructure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func TestHash_equal(t *testing.T) {
{
struct{ Lname, Fname string }{"foo", "bar"},
struct{ Fname, Lname string }{"bar", "foo"},
true,
false,
},

{
Expand Down

0 comments on commit d5de409

Please sign in to comment.