Workaround to modify a const value without the protection from @constCast, yet the test expects initialization value

so, I was writing a store for dynamic value (just a wrapper for std.HashMap) and found out that you can modify a const value by passing its pointer then doing a asBytes()

the file here [should I paste it here]

// بسم الله الرحمن الرحيم
// la ilaha illa Allah Mohammed Rassoul Allah

map: std.StringArrayHashMap([]u8),

pub fn init(allocator: std.mem.Allocator) Self {
    return .{
        .map = .init(allocator),
    };
}

pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
    var iter = self.map.iterator();
    while (iter.next()) |entry| allocator.free(entry.value_ptr.*);
    self.map.deinit();
}

/// adds the value itself to the map after duping it
///
/// calling it like `try map.add("123", 123, allocator);`
/// whill store a heap allocated int
pub fn add(self: *Self, key: []const u8, value: anytype, allocator: std.mem.Allocator) !void {
    const slice: []const u8 = std.mem.asBytes(&value);
    const slice_allocated: []u8 = try allocator.dupe(u8, slice);
    errdefer allocator.free(slice_allocated);
    try self.map.put(key, slice_allocated);
}

pub fn get(self: *Self, key: []const u8) ?[]u8 {
    return self.map.get(key);
}

pub fn getAs(self: *Self, key: []const u8, T: type) ?*T {
    const raw: []u8 = self.map.get(key) orelse return null;
    const ptr: *T = @alignCast(std.mem.bytesAsValue(T, raw));
    return ptr;
}

test "multiple types" {
    const testing = std.testing;
    var store: Self = .init(testing.allocator);
    defer store.deinit(testing.allocator);

    var a: u32 = 31;
    try store.add("a", a, testing.allocator);
    try store.add("a_ptr", &a, testing.allocator);

    const a_entry: *u32 = store.getAs("a", u32) orelse unreachable;
    a_entry.* = 0;
    try testing.expect(a == 31);
    try testing.expect(a_entry.* == 0);

    const a_ptr_entry: **u32 = store.getAs("a_ptr", *u32) orelse unreachable;
    a_ptr_entry.*.* = 8;
    try testing.expect(a_entry.* == 0);
    try testing.expect(a_ptr_entry.* != a_entry);
    try testing.expectEqual(a_ptr_entry.*, &a);
    try testing.expectEqual(a, 8);
}

const Self = @This();
const std = @import("std");

I get error: 'store.test.multiple types' failed: expected 31, found 8 unless I change const a: u32 = 31; to var a: u32 = 31;

the question is:

why does try testing.expectEqual(a_ptr_entry.*, &a); pass and at the same time try testing.expectEqual(a, 8); fails? I assume that for the latter it checks for a at comptime because a is a const so any modifications to it might be a programmer bug.

final note

dis is a programmer’s fault probably, idk if this is allowed to be considered a bug. it’s just: idk.

The type is small, it is const with a comptime known value; so the compiler can avoid loading it from memory, instead embedding the known value into the instructions as an optimisation.

The error is your code not preserving the constness of the pointer, which then allows it to modify the value which is illegal behaviour.

What is surprising is that, despite being const and comptime known, a is for some reason not in read only memory, or even in memory at all. Either of those would crash your program when trying to write to it. That could certainly be fixed to make this programmer error more obvious.

Coincidently there was a similar post today How to explain the behavior of modifying the value referenced by the result of @constCast? - #5 by npc1054657282

Btw @constCast offers no protection against this, as shown by the linked post.
zig believes you when you lie to it, because it has no way to know if you’re lying or not.

2 Likes

`@constCast` is like saying: “I own this computer and you work for me, follow my orders without question“ or something so it should not provide any protection

1 Like