std: introduce pointer stability locks to hash maps (#17719)
This adds std.debug.SafetyLock and uses it in std.HashMapUnmanaged by adding lockPointers() and unlockPointers(). This provides a way to detect when an illegal modification has happened and panic rather than invoke undefined behavior.
This commit is contained in:
@@ -137,6 +137,23 @@ pub fn ArrayHashMap(
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// Puts the hash map into a state where any method call that would
|
||||
/// cause an existing key or value pointer to become invalidated will
|
||||
/// instead trigger an assertion.
|
||||
///
|
||||
/// An additional call to `lockPointers` in such state also triggers an
|
||||
/// assertion.
|
||||
///
|
||||
/// `unlockPointers` returns the hash map to the previous state.
|
||||
pub fn lockPointers(self: *Self) void {
|
||||
self.unmanaged.lockPointers();
|
||||
}
|
||||
|
||||
/// Undoes a call to `lockPointers`.
|
||||
pub fn unlockPointers(self: *Self) void {
|
||||
self.unmanaged.unlockPointers();
|
||||
}
|
||||
|
||||
/// Clears the map but retains the backing allocation for future use.
|
||||
pub fn clearRetainingCapacity(self: *Self) void {
|
||||
return self.unmanaged.clearRetainingCapacity();
|
||||
@@ -403,6 +420,7 @@ pub fn ArrayHashMap(
|
||||
/// Set the map to an empty state, making deinitialization a no-op, and
|
||||
/// returning a copy of the original.
|
||||
pub fn move(self: *Self) Self {
|
||||
self.pointer_stability.assertUnlocked();
|
||||
const result = self.*;
|
||||
self.unmanaged = .{};
|
||||
return result;
|
||||
@@ -495,6 +513,9 @@ pub fn ArrayHashMapUnmanaged(
|
||||
/// by how many total indexes there are.
|
||||
index_header: ?*IndexHeader = null,
|
||||
|
||||
/// Used to detect memory safety violations.
|
||||
pointer_stability: std.debug.SafetyLock = .{},
|
||||
|
||||
comptime {
|
||||
std.hash_map.verifyContext(Context, K, K, u32, true);
|
||||
}
|
||||
@@ -589,6 +610,7 @@ pub fn ArrayHashMapUnmanaged(
|
||||
/// Note that this does not free keys or values. You must take care of that
|
||||
/// before calling this function, if it is needed.
|
||||
pub fn deinit(self: *Self, allocator: Allocator) void {
|
||||
self.pointer_stability.assertUnlocked();
|
||||
self.entries.deinit(allocator);
|
||||
if (self.index_header) |header| {
|
||||
header.free(allocator);
|
||||
@@ -596,8 +618,28 @@ pub fn ArrayHashMapUnmanaged(
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// Puts the hash map into a state where any method call that would
|
||||
/// cause an existing key or value pointer to become invalidated will
|
||||
/// instead trigger an assertion.
|
||||
///
|
||||
/// An additional call to `lockPointers` in such state also triggers an
|
||||
/// assertion.
|
||||
///
|
||||
/// `unlockPointers` returns the hash map to the previous state.
|
||||
pub fn lockPointers(self: *Self) void {
|
||||
self.pointer_stability.lock();
|
||||
}
|
||||
|
||||
/// Undoes a call to `lockPointers`.
|
||||
pub fn unlockPointers(self: *Self) void {
|
||||
self.pointer_stability.unlock();
|
||||
}
|
||||
|
||||
/// Clears the map but retains the backing allocation for future use.
|
||||
pub fn clearRetainingCapacity(self: *Self) void {
|
||||
self.pointer_stability.lock();
|
||||
defer self.pointer_stability.unlock();
|
||||
|
||||
self.entries.len = 0;
|
||||
if (self.index_header) |header| {
|
||||
switch (header.capacityIndexType()) {
|
||||
@@ -610,6 +652,9 @@ pub fn ArrayHashMapUnmanaged(
|
||||
|
||||
/// Clears the map and releases the backing allocation
|
||||
pub fn clearAndFree(self: *Self, allocator: Allocator) void {
|
||||
self.pointer_stability.lock();
|
||||
defer self.pointer_stability.unlock();
|
||||
|
||||
self.entries.shrinkAndFree(allocator, 0);
|
||||
if (self.index_header) |header| {
|
||||
header.free(allocator);
|
||||
@@ -795,6 +840,9 @@ pub fn ArrayHashMapUnmanaged(
|
||||
return self.ensureTotalCapacityContext(allocator, new_capacity, undefined);
|
||||
}
|
||||
pub fn ensureTotalCapacityContext(self: *Self, allocator: Allocator, new_capacity: usize, ctx: Context) !void {
|
||||
self.pointer_stability.lock();
|
||||
defer self.pointer_stability.unlock();
|
||||
|
||||
if (new_capacity <= linear_scan_max) {
|
||||
try self.entries.ensureTotalCapacity(allocator, new_capacity);
|
||||
return;
|
||||
@@ -1079,6 +1127,9 @@ pub fn ArrayHashMapUnmanaged(
|
||||
return self.fetchSwapRemoveContextAdapted(key, ctx, undefined);
|
||||
}
|
||||
pub fn fetchSwapRemoveContextAdapted(self: *Self, key: anytype, key_ctx: anytype, ctx: Context) ?KV {
|
||||
self.pointer_stability.lock();
|
||||
defer self.pointer_stability.unlock();
|
||||
|
||||
return self.fetchRemoveByKey(key, key_ctx, if (store_hash) {} else ctx, .swap);
|
||||
}
|
||||
|
||||
@@ -1100,6 +1151,9 @@ pub fn ArrayHashMapUnmanaged(
|
||||
return self.fetchOrderedRemoveContextAdapted(key, ctx, undefined);
|
||||
}
|
||||
pub fn fetchOrderedRemoveContextAdapted(self: *Self, key: anytype, key_ctx: anytype, ctx: Context) ?KV {
|
||||
self.pointer_stability.lock();
|
||||
defer self.pointer_stability.unlock();
|
||||
|
||||
return self.fetchRemoveByKey(key, key_ctx, if (store_hash) {} else ctx, .ordered);
|
||||
}
|
||||
|
||||
@@ -1121,6 +1175,9 @@ pub fn ArrayHashMapUnmanaged(
|
||||
return self.swapRemoveContextAdapted(key, ctx, undefined);
|
||||
}
|
||||
pub fn swapRemoveContextAdapted(self: *Self, key: anytype, key_ctx: anytype, ctx: Context) bool {
|
||||
self.pointer_stability.lock();
|
||||
defer self.pointer_stability.unlock();
|
||||
|
||||
return self.removeByKey(key, key_ctx, if (store_hash) {} else ctx, .swap);
|
||||
}
|
||||
|
||||
@@ -1142,6 +1199,9 @@ pub fn ArrayHashMapUnmanaged(
|
||||
return self.orderedRemoveContextAdapted(key, ctx, undefined);
|
||||
}
|
||||
pub fn orderedRemoveContextAdapted(self: *Self, key: anytype, key_ctx: anytype, ctx: Context) bool {
|
||||
self.pointer_stability.lock();
|
||||
defer self.pointer_stability.unlock();
|
||||
|
||||
return self.removeByKey(key, key_ctx, if (store_hash) {} else ctx, .ordered);
|
||||
}
|
||||
|
||||
@@ -1154,6 +1214,9 @@ pub fn ArrayHashMapUnmanaged(
|
||||
return self.swapRemoveAtContext(index, undefined);
|
||||
}
|
||||
pub fn swapRemoveAtContext(self: *Self, index: usize, ctx: Context) void {
|
||||
self.pointer_stability.lock();
|
||||
defer self.pointer_stability.unlock();
|
||||
|
||||
self.removeByIndex(index, if (store_hash) {} else ctx, .swap);
|
||||
}
|
||||
|
||||
@@ -1167,6 +1230,9 @@ pub fn ArrayHashMapUnmanaged(
|
||||
return self.orderedRemoveAtContext(index, undefined);
|
||||
}
|
||||
pub fn orderedRemoveAtContext(self: *Self, index: usize, ctx: Context) void {
|
||||
self.pointer_stability.lock();
|
||||
defer self.pointer_stability.unlock();
|
||||
|
||||
self.removeByIndex(index, if (store_hash) {} else ctx, .ordered);
|
||||
}
|
||||
|
||||
@@ -1196,6 +1262,7 @@ pub fn ArrayHashMapUnmanaged(
|
||||
/// Set the map to an empty state, making deinitialization a no-op, and
|
||||
/// returning a copy of the original.
|
||||
pub fn move(self: *Self) Self {
|
||||
self.pointer_stability.assertUnlocked();
|
||||
const result = self.*;
|
||||
self.* = .{};
|
||||
return result;
|
||||
@@ -1271,6 +1338,9 @@ pub fn ArrayHashMapUnmanaged(
|
||||
sort_ctx: anytype,
|
||||
ctx: Context,
|
||||
) void {
|
||||
self.pointer_stability.lock();
|
||||
defer self.pointer_stability.unlock();
|
||||
|
||||
switch (mode) {
|
||||
.stable => self.entries.sort(sort_ctx),
|
||||
.unstable => self.entries.sortUnstable(sort_ctx),
|
||||
@@ -1288,6 +1358,9 @@ pub fn ArrayHashMapUnmanaged(
|
||||
return self.shrinkRetainingCapacityContext(new_len, undefined);
|
||||
}
|
||||
pub fn shrinkRetainingCapacityContext(self: *Self, new_len: usize, ctx: Context) void {
|
||||
self.pointer_stability.lock();
|
||||
defer self.pointer_stability.unlock();
|
||||
|
||||
// Remove index entries from the new length onwards.
|
||||
// Explicitly choose to ONLY remove index entries and not the underlying array list
|
||||
// entries as we're going to remove them in the subsequent shrink call.
|
||||
@@ -1307,6 +1380,9 @@ pub fn ArrayHashMapUnmanaged(
|
||||
return self.shrinkAndFreeContext(allocator, new_len, undefined);
|
||||
}
|
||||
pub fn shrinkAndFreeContext(self: *Self, allocator: Allocator, new_len: usize, ctx: Context) void {
|
||||
self.pointer_stability.lock();
|
||||
defer self.pointer_stability.unlock();
|
||||
|
||||
// Remove index entries from the new length onwards.
|
||||
// Explicitly choose to ONLY remove index entries and not the underlying array list
|
||||
// entries as we're going to remove them in the subsequent shrink call.
|
||||
@@ -1325,6 +1401,9 @@ pub fn ArrayHashMapUnmanaged(
|
||||
return self.popContext(undefined);
|
||||
}
|
||||
pub fn popContext(self: *Self, ctx: Context) KV {
|
||||
self.pointer_stability.lock();
|
||||
defer self.pointer_stability.unlock();
|
||||
|
||||
const item = self.entries.get(self.entries.len - 1);
|
||||
if (self.index_header) |header|
|
||||
self.removeFromIndexByIndex(self.entries.len - 1, if (store_hash) {} else ctx, header);
|
||||
@@ -1346,9 +1425,13 @@ pub fn ArrayHashMapUnmanaged(
|
||||
return if (self.entries.len == 0) null else self.popContext(ctx);
|
||||
}
|
||||
|
||||
// ------------------ No pub fns below this point ------------------
|
||||
|
||||
fn fetchRemoveByKey(self: *Self, key: anytype, key_ctx: anytype, ctx: ByIndexContext, comptime removal_type: RemovalType) ?KV {
|
||||
fn fetchRemoveByKey(
|
||||
self: *Self,
|
||||
key: anytype,
|
||||
key_ctx: anytype,
|
||||
ctx: ByIndexContext,
|
||||
comptime removal_type: RemovalType,
|
||||
) ?KV {
|
||||
const header = self.index_header orelse {
|
||||
// Linear scan.
|
||||
const key_hash = if (store_hash) key_ctx.hash(key) else {};
|
||||
@@ -1377,7 +1460,15 @@ pub fn ArrayHashMapUnmanaged(
|
||||
.u32 => self.fetchRemoveByKeyGeneric(key, key_ctx, ctx, header, u32, removal_type),
|
||||
};
|
||||
}
|
||||
fn fetchRemoveByKeyGeneric(self: *Self, key: anytype, key_ctx: anytype, ctx: ByIndexContext, header: *IndexHeader, comptime I: type, comptime removal_type: RemovalType) ?KV {
|
||||
fn fetchRemoveByKeyGeneric(
|
||||
self: *Self,
|
||||
key: anytype,
|
||||
key_ctx: anytype,
|
||||
ctx: ByIndexContext,
|
||||
header: *IndexHeader,
|
||||
comptime I: type,
|
||||
comptime removal_type: RemovalType,
|
||||
) ?KV {
|
||||
const indexes = header.indexes(I);
|
||||
const entry_index = self.removeFromIndexByKey(key, key_ctx, header, I, indexes) orelse return null;
|
||||
const slice = self.entries.slice();
|
||||
@@ -1389,7 +1480,13 @@ pub fn ArrayHashMapUnmanaged(
|
||||
return removed_entry;
|
||||
}
|
||||
|
||||
fn removeByKey(self: *Self, key: anytype, key_ctx: anytype, ctx: ByIndexContext, comptime removal_type: RemovalType) bool {
|
||||
fn removeByKey(
|
||||
self: *Self,
|
||||
key: anytype,
|
||||
key_ctx: anytype,
|
||||
ctx: ByIndexContext,
|
||||
comptime removal_type: RemovalType,
|
||||
) bool {
|
||||
const header = self.index_header orelse {
|
||||
// Linear scan.
|
||||
const key_hash = if (store_hash) key_ctx.hash(key) else {};
|
||||
|
||||
Reference in New Issue
Block a user