aboutsummaryrefslogtreecommitdiff
path: root/src/install/padding_checker.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/install/padding_checker.zig')
-rw-r--r--src/install/padding_checker.zig93
1 files changed, 93 insertions, 0 deletions
diff --git a/src/install/padding_checker.zig b/src/install/padding_checker.zig
new file mode 100644
index 000000000..1d9405a43
--- /dev/null
+++ b/src/install/padding_checker.zig
@@ -0,0 +1,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);
+ }
+}