aboutsummaryrefslogtreecommitdiff
path: root/src/install/padding_checker.zig
blob: 1d9405a43d311195ce6c1e2aeae4a0ec841af4db (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
const std = @import("std");

/// In some parts of lockfile serialization, Bun will use `std.mem.sliceAsBytes` to convert a struct into raw
/// bytes to write. This makes lockfile serialization/deserialization much simpler/faster, at the cost of not
/// having any pointers within these structs.
///
/// One major caveat of this is that if any of these structs have uninitialized memory, then that can leak
/// garbage memory into the lockfile. See https://github.com/oven-sh/bun/issues/4319
///
/// The obvious way to introduce undefined memory into a struct is via `.field = undefined`, but a much more
/// subtle way is to have implicit padding in an extern struct. For example:
/// ```zig
/// const Demo = struct {
///     a: u8,  // @sizeOf(Demo, "a") == 1,   @offsetOf(Demo, "a") == 0
///     b: u64, // @sizeOf(Demo, "b") == 8,   @offsetOf(Demo, "b") == 8
/// }
/// ```
///
/// `a` is only one byte long, but due to the alignment of `b`, there is 7 bytes of padding between `a` and `b`,
/// which is considered *undefined memory*.
///
/// The solution is to have it explicitly initialized to zero bytes, like:
/// ```zig
/// const Demo = struct {
///     a: u8,
///     _padding: [7]u8 = .{0} ** 7,
///     b: u64, // same offset as before
/// }
/// ```
///
/// There is one other way to introduce undefined memory into a struct, which this does not check for, and that is
/// a union with unequal size fields.
pub fn assertNoUninitializedPadding(comptime T: type) void {
    const info_ = @typeInfo(T);
    const info = switch (info_) {
        .Struct => info_.Struct,
        .Union => info_.Union,
        .Array => |a| {
            assertNoUninitializedPadding(a.child);
            return;
        },
        .Optional => |a| {
            assertNoUninitializedPadding(a.child);
            return;
        },
        .Pointer => |ptr| {
            // Pointers aren't allowed, but this just makes the assertion easier to invoke.
            assertNoUninitializedPadding(ptr.child);
            return;
        },
        else => {
            return;
        },
    };
    // if (info.layout != .Extern) {
    //     @compileError("assertNoUninitializedPadding(" ++ @typeName(T) ++ ") expects an extern struct type, got a struct of layout '" ++ @tagName(info.layout) ++ "'");
    // }
    var i = 0;
    for (info.fields) |field| {
        const fieldInfo = @typeInfo(field.type);
        switch (fieldInfo) {
            .Struct => assertNoUninitializedPadding(field.type),
            .Union => assertNoUninitializedPadding(field.type),
            .Array => |a| assertNoUninitializedPadding(a.child),
            .Optional => |a| assertNoUninitializedPadding(a.child),
            .Pointer => {
                @compileError("Expected no pointer types in " ++ @typeName(T) ++ ", found field '" ++ field.name ++ "' of type '" ++ @typeName(field.type) ++ "'");
            },
            else => {},
        }
    }
    if (info_ == .Union) {
        return;
    }
    for (info.fields, 0..) |field, j| {
        const offset = @offsetOf(T, field.name);
        if (offset != i) {
            @compileError(std.fmt.comptimePrint(
                \\Expected no possibly uninitialized bytes of memory in '{s}', but found a {d} byte gap between fields '{s}' and '{s}' This can be fixed by adding a padding field to the struct like `padding: [{d}]u8 = .{{0}} ** {d},` between these fields. For more information, look at `padding_checker.zig`
            ,
                .{
                    @typeName(T),
                    offset - i,
                    info.fields[j - 1].name,
                    field.name,
                    offset - i,
                    offset - i,
                },
            ));
        }
        i = offset + @sizeOf(field.type);
    }
}