Skip to content

Commit b010833

Browse files
authored
Merge pull request #15207 from lorentey/seedless-hashes
[runtime] Add better control over the random hashing seed
2 parents 274c109 + e5eed66 commit b010833

File tree

8 files changed

+112
-89
lines changed

8 files changed

+112
-89
lines changed

stdlib/private/StdlibUnittest/StdlibUnittest.swift.gyb

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,14 +1091,6 @@ struct PersistentState {
10911091
static var complaintInstalled = false
10921092
static var hashingKeyOverridden = false
10931093

1094-
static func overrideHashingKey() {
1095-
if !hashingKeyOverridden {
1096-
// FIXME(hasher): This has to run before creating the first Set/Dictionary
1097-
_Hasher._secretKey = (0, 0)
1098-
hashingKeyOverridden = true
1099-
}
1100-
}
1101-
11021094
static func complainIfNothingRuns() {
11031095
if !complaintInstalled {
11041096
complaintInstalled = true
@@ -1209,7 +1201,6 @@ func stopTrackingObjects(_: UnsafePointer<CChar>) -> Int
12091201

12101202
public final class TestSuite {
12111203
public init(_ name: String) {
1212-
PersistentState.overrideHashingKey()
12131204
self.name = name
12141205
_precondition(
12151206
_testNameToIndex[name] == nil,

stdlib/public/SwiftShims/GlobalObjects.h

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#define SWIFT_STDLIB_SHIMS_GLOBALOBJECTS_H_
2020

2121
#include "SwiftStdint.h"
22+
#include "SwiftStdbool.h"
2223
#include "HeapObject.h"
2324
#include "Visibility.h"
2425

@@ -77,16 +78,16 @@ struct _SwiftEmptyDictionaryStorage _swiftEmptyDictionaryStorage;
7778
SWIFT_RUNTIME_STDLIB_INTERFACE
7879
struct _SwiftEmptySetStorage _swiftEmptySetStorage;
7980

80-
struct _SwiftHashingSecretKey {
81-
__swift_uint64_t key0;
82-
__swift_uint64_t key1;
81+
struct _SwiftHashingSeed {
82+
__swift_uint64_t seed0;
83+
__swift_uint64_t seed1;
8384
};
8485

8586
SWIFT_RUNTIME_STDLIB_INTERFACE
86-
struct _SwiftHashingSecretKey _swift_stdlib_Hashing_secretKey;
87+
struct _SwiftHashingSeed _swift_stdlib_Hashing_seed;
8788

8889
SWIFT_RUNTIME_STDLIB_INTERFACE
89-
__swift_uint64_t _swift_stdlib_HashingDetail_fixedSeedOverride;
90+
__swift_bool _swift_stdlib_Hashing_deterministicHashing;
9091

9192
#ifdef __cplusplus
9293

stdlib/public/core/Hashing.swift

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,30 +27,14 @@ import SwiftShims
2727
public // @testable
2828
enum _HashingDetail {
2929

30-
// FIXME(hasher): Remove
31-
@_inlineable // FIXME(sil-serialize-all)
32-
public // @testable
33-
static var fixedSeedOverride: UInt64 {
34-
get {
35-
// HACK: the variable itself is defined in C++ code so that it is
36-
// guaranteed to be statically initialized. This is a temporary
37-
// workaround until the compiler can do the same for Swift.
38-
return _swift_stdlib_HashingDetail_fixedSeedOverride
39-
}
40-
set {
41-
_swift_stdlib_HashingDetail_fixedSeedOverride = newValue
42-
}
43-
}
44-
4530
// FIXME(hasher): Remove
4631
@_inlineable // FIXME(sil-serialize-all)
4732
@_versioned
4833
@_transparent
4934
internal static func getExecutionSeed() -> UInt64 {
5035
// FIXME: This needs to be a per-execution seed. This is just a placeholder
5136
// implementation.
52-
let seed: UInt64 = 0xff51afd7ed558ccd
53-
return _HashingDetail.fixedSeedOverride == 0 ? seed : fixedSeedOverride
37+
return 0xff51afd7ed558ccd
5438
}
5539

5640
// FIXME(hasher): Remove
@@ -224,33 +208,39 @@ public struct _Hasher {
224208
// NOT @_inlineable
225209
@effects(releasenone)
226210
public init() {
227-
self._core = Core(key: _Hasher._secretKey)
211+
self._core = Core(key: _Hasher._seed)
228212
}
229213

230214
// NOT @_inlineable
231215
@effects(releasenone)
232-
public init(key: (UInt64, UInt64)) {
233-
self._core = Core(key: key)
216+
public init(seed: (UInt64, UInt64)) {
217+
self._core = Core(key: seed)
234218
}
235219

236-
// FIXME(ABI)#41 : make this an actual public API.
237-
@_inlineable // FIXME(sil-serialize-all)
220+
/// Indicates whether we're running in an environment where hashing needs to
221+
/// be deterministic. If this is true, the hash seed is not random, and hash
222+
/// tables do not apply per-instance perturbation that is not repeatable.
223+
/// This is not recommended for production use, but it is useful in certain
224+
/// test environments where randomization may lead to unwanted nondeterminism
225+
/// of test results.
226+
public // SPI
227+
static var _isDeterministic: Bool {
228+
return _swift_stdlib_Hashing_deterministicHashing
229+
}
230+
231+
/// The 128-bit hash seed used to initialize the hasher state. Initialized
232+
/// once during process startup.
238233
public // SPI
239-
static var _secretKey: (UInt64, UInt64) {
234+
static var _seed: (UInt64, UInt64) {
240235
get {
241-
// The variable itself is defined in C++ code so that it is initialized
242-
// during static construction. Almost every Swift program uses hash
243-
// tables, so initializing the secret key during the startup seems to be
244-
// the right trade-off.
236+
if _isDeterministic { return (0, 0) }
237+
// The seed itself is defined in C++ code so that it is initialized during
238+
// static construction. Almost every Swift program uses hash tables, so
239+
// initializing the seed during the startup seems to be the right
240+
// trade-off.
245241
return (
246-
_swift_stdlib_Hashing_secretKey.key0,
247-
_swift_stdlib_Hashing_secretKey.key1)
248-
}
249-
set {
250-
// FIXME(hasher) Replace setter with some override mechanism inside
251-
// the runtime
252-
(_swift_stdlib_Hashing_secretKey.key0,
253-
_swift_stdlib_Hashing_secretKey.key1) = newValue
242+
_swift_stdlib_Hashing_seed.seed0,
243+
_swift_stdlib_Hashing_seed.seed1)
254244
}
255245
}
256246

stdlib/public/stubs/GlobalObjects.cpp

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,20 +106,37 @@ swift::_SwiftEmptySetStorage swift::_swiftEmptySetStorage = {
106106
0 // int entries; (zero'd bits)
107107
};
108108

109-
static __swift_uint64_t randomUInt64() {
109+
static swift::_SwiftHashingSeed initializeHashingSeed() {
110+
#if defined(__APPLE__)
111+
// Use arc4random if available.
112+
swift::_SwiftHashingSeed seed = { 0, 0 };
113+
arc4random_buf(&seed, sizeof(seed));
114+
return seed;
115+
#else
110116
std::random_device randomDevice;
111-
std::mt19937_64 twisterEngine(randomDevice());
117+
std::mt19937_64 engine(randomDevice());
112118
std::uniform_int_distribution<__swift_uint64_t> distribution;
113-
return distribution(twisterEngine);
119+
return { distribution(engine), distribution(engine) };
120+
#endif
121+
}
122+
123+
static __swift_bool initializeHashingDeterminism() {
124+
// Setting the environment variable SWIFT_DETERMINISTIC_HASHING to "1"
125+
// disables randomized hash seeding. This is useful in cases we need to ensure
126+
// results are repeatable, e.g., in certain test environments. (Note that
127+
// even if the seed override is enabled, hash values aren't guaranteed to
128+
// remain stable across even minor stdlib releases.)
129+
auto determinism = getenv("SWIFT_DETERMINISTIC_HASHING");
130+
return determinism && 0 == strcmp(determinism, "1");
114131
}
115132

116133
SWIFT_ALLOWED_RUNTIME_GLOBAL_CTOR_BEGIN
117-
swift::_SwiftHashingSecretKey swift::_swift_stdlib_Hashing_secretKey = {
118-
randomUInt64(), randomUInt64()
119-
};
134+
swift::_SwiftHashingSeed swift::_swift_stdlib_Hashing_seed =
135+
initializeHashingSeed();
136+
__swift_bool swift::_swift_stdlib_Hashing_deterministicHashing =
137+
initializeHashingDeterminism();
120138
SWIFT_ALLOWED_RUNTIME_GLOBAL_CTOR_END
121139

122-
__swift_uint64_t swift::_swift_stdlib_HashingDetail_fixedSeedOverride = 0;
123140

124141
SWIFT_RUNTIME_STDLIB_INTERFACE
125142
void swift::_swift_instantiateInertHeapObject(void *address,

test/lit.cfg

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -914,7 +914,8 @@ ENV_VAR_PREFIXES = {
914914
'watchsimulator': SIMULATOR_ENV_PREFIX,
915915
'appletvsimulator': SIMULATOR_ENV_PREFIX
916916
}
917-
config.substitutions.append(('%env-', ENV_VAR_PREFIXES.get(config.target_sdk_name, "")))
917+
TARGET_ENV_PREFIX = ENV_VAR_PREFIXES.get(config.target_sdk_name, "")
918+
config.substitutions.append(('%env-', TARGET_ENV_PREFIX))
918919
config.substitutions.append(("%target-sdk-name", config.target_sdk_name))
919920

920921
config.compiler_rt_libs = []
@@ -1134,6 +1135,10 @@ if config.lldb_build_root != "":
11341135
python_lib_dir = get_python_lib(True, False, config.lldb_build_root)
11351136
config.substitutions.append(('%lldb-python-path', python_lib_dir))
11361137

1138+
# Disable randomized hash seeding by default. Tests need to manually opt in to
1139+
# random seeds by unsetting the SWIFT_DETERMINISTIC_HASHING environment
1140+
# variable.
1141+
config.environment[TARGET_ENV_PREFIX + 'SWIFT_DETERMINISTIC_HASHING'] = '1'
11371142

11381143
# Run lsb_release on the target to be tested and return the results.
11391144
def linux_get_lsb_release():

validation-test/stdlib/FixedPoint.swift.gyb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ FixedPoint.test("${Self}.hashValue") {
242242
let input = get${Self}(${input})
243243
let output = getInt(input.hashValue)
244244

245-
var hasher = _SipHash13(key: _Hasher._secretKey)
245+
var hasher = _SipHash13(key: _Hasher._seed)
246246
% if prepare_bit_pattern(input, word_bits, self_ty.is_signed) == input:
247247
hasher.append(${input} as ${"" if self_ty.is_signed else "U"}Int)
248248
% else:

validation-test/stdlib/Hashing.swift

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ var HashingTestSuite = TestSuite("Hashing")
1010

1111
func checkHash(
1212
for value: UInt64,
13-
withKey key: (UInt64, UInt64),
13+
withSeed seed: (UInt64, UInt64),
1414
expected: UInt64,
1515
file: String = #file, line: UInt = #line
1616
) {
17-
var hasher = _Hasher(key: key)
17+
var hasher = _Hasher(seed: seed)
1818
hasher.append(bits: value)
1919
let hash = hasher.finalize()
2020
expectEqual(
@@ -24,23 +24,23 @@ func checkHash(
2424

2525
HashingTestSuite.test("_Hasher/CustomKeys") {
2626
// This assumes _Hasher implements SipHash-1-3.
27-
checkHash(for: 0, withKey: (0, 0), expected: 0xbd60acb658c79e45)
28-
checkHash(for: 0, withKey: (0, 1), expected: 0x1ce32b0b44e61175)
29-
checkHash(for: 0, withKey: (1, 0), expected: 0x9c44b7c8df2ca74b)
30-
checkHash(for: 0, withKey: (1, 1), expected: 0x9653ca0a3b455506)
31-
checkHash(for: 0, withKey: (.max, .max), expected: 0x3ab336a4895e4d36)
32-
33-
checkHash(for: 1, withKey: (0, 0), expected: 0x1e9f734161d62dd9)
34-
checkHash(for: 1, withKey: (0, 1), expected: 0xb6fcf32d09f76cba)
35-
checkHash(for: 1, withKey: (1, 0), expected: 0xacb556b13007504a)
36-
checkHash(for: 1, withKey: (1, 1), expected: 0x7defec680db51d24)
37-
checkHash(for: 1, withKey: (.max, .max), expected: 0x212798441870ef6b)
38-
39-
checkHash(for: .max, withKey: (0, 0), expected: 0x2f205be2fec8e38d)
40-
checkHash(for: .max, withKey: (0, 1), expected: 0x3ff7fa33381ecf7b)
41-
checkHash(for: .max, withKey: (1, 0), expected: 0x404afd8eb2c4b22a)
42-
checkHash(for: .max, withKey: (1, 1), expected: 0x855642d657c1bd46)
43-
checkHash(for: .max, withKey: (.max, .max), expected: 0x5b16b7a8181980c2)
27+
checkHash(for: 0, withSeed: (0, 0), expected: 0xbd60acb658c79e45)
28+
checkHash(for: 0, withSeed: (0, 1), expected: 0x1ce32b0b44e61175)
29+
checkHash(for: 0, withSeed: (1, 0), expected: 0x9c44b7c8df2ca74b)
30+
checkHash(for: 0, withSeed: (1, 1), expected: 0x9653ca0a3b455506)
31+
checkHash(for: 0, withSeed: (.max, .max), expected: 0x3ab336a4895e4d36)
32+
33+
checkHash(for: 1, withSeed: (0, 0), expected: 0x1e9f734161d62dd9)
34+
checkHash(for: 1, withSeed: (0, 1), expected: 0xb6fcf32d09f76cba)
35+
checkHash(for: 1, withSeed: (1, 0), expected: 0xacb556b13007504a)
36+
checkHash(for: 1, withSeed: (1, 1), expected: 0x7defec680db51d24)
37+
checkHash(for: 1, withSeed: (.max, .max), expected: 0x212798441870ef6b)
38+
39+
checkHash(for: .max, withSeed: (0, 0), expected: 0x2f205be2fec8e38d)
40+
checkHash(for: .max, withSeed: (0, 1), expected: 0x3ff7fa33381ecf7b)
41+
checkHash(for: .max, withSeed: (1, 0), expected: 0x404afd8eb2c4b22a)
42+
checkHash(for: .max, withSeed: (1, 1), expected: 0x855642d657c1bd46)
43+
checkHash(for: .max, withSeed: (.max, .max), expected: 0x5b16b7a8181980c2)
4444
}
4545

4646
HashingTestSuite.test("_Hasher/DefaultKey") {
@@ -52,22 +52,15 @@ HashingTestSuite.test("_Hasher/DefaultKey") {
5252
defaultHasher.append(bits: value)
5353
expectEqual(defaultHasher.finalize(), defaultHash)
5454

55-
var customHasher = _Hasher(key: _Hasher._secretKey)
55+
var customHasher = _Hasher(seed: _Hasher._seed)
5656
customHasher.append(bits: value)
5757
expectEqual(customHasher.finalize(), defaultHash)
5858
}
5959

60-
HashingTestSuite.test("_Hasher/keyOverride") {
61-
let value: UInt64 = 0x0102030405060708
62-
let expected = Int(truncatingIfNeeded: 0x661dac5d71c78013 as UInt64)
63-
64-
let originalKey = _Hasher._secretKey
65-
_Hasher._secretKey = (1, 2)
66-
let hash = _hashValue(for: value)
67-
_Hasher._secretKey = originalKey
68-
69-
expectEqual(hash, expected)
60+
HashingTestSuite.test("_Hasher/determinism") {
61+
// By defaults, tests are configured to run with deterministic hashing.
62+
expectTrue(_Hasher._isDeterministic)
63+
expectEqual((0, 0), _Hasher._seed)
7064
}
7165

7266
runAllTests()
73-
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-build-swift -module-name main %s -o %t/hash
3+
// RUN: (export -n %env-SWIFT_DETERMINISTIC_HASHING; %target-run %t/hash && %target-run %t/hash) | %FileCheck %s
4+
// REQUIRES: executable_test
5+
6+
// This check verifies that the hash seed is randomly generated on every
7+
// execution of a Swift program. There is a minuscule chance that the same seed
8+
// is generated on two separate executions; however, a test failure here is more
9+
// likely to indicate an issue with the random number generator or the testing
10+
// environment.
11+
12+
print("Deterministic: \(_Hasher._isDeterministic)")
13+
print("Seed: \(_Hasher._seed)")
14+
print("Hash values: <\(0.hashValue), \(1.hashValue)>")
15+
16+
// On the first run, remember the seed and hash value.
17+
// CHECK: Deterministic: false
18+
// CHECK-NEXT: Seed: [[SEED0:\([0-9]+, [0-9]+\)]]
19+
// CHECK-NEXT: Hash values: [[HASH0:<-?[0-9]+, -?[0-9]+>]]
20+
21+
// Check that the values are different on the second run.
22+
// CHECK-NEXT: Deterministic: false
23+
// CHECK-NEXT: Seed:
24+
// CHECK-NOT: [[SEED0]]
25+
// CHECK-NEXT: Hash values:
26+
// CHECK-NOT: [[HASH0]]

0 commit comments

Comments
 (0)