diff --git a/src/deserialize.zig b/src/deserialize.zig index 4d61367..e28761a 100644 --- a/src/deserialize.zig +++ b/src/deserialize.zig @@ -17,26 +17,16 @@ const rlpListLongHeader = 247; // difference in byte-size between the number of bytes and the target integer. // If so, the bytes have to be extracted into a temporary value. inline fn safeReadSliceIntBig(comptime T: type, payload: []const u8, out: *T) !void { - // compile time constat to activate the first branch. If - // @sizeOf(T) > 1, then it is possible (and necessary) to - // shift temp. - const log2tgt1 = (@sizeOf(T) > 1); - if (log2tgt1 and @sizeOf(T) > payload.len) { - var temp: T = 0; - var i: usize = 0; - while (i < payload.len) : (i += 1) { - temp = @shlExact(temp, 8); // payload.len < @sizeOf(T), should not overflow - temp |= @as(T, payload[i]); - } - out.* = temp; - } else { - out.* = std.mem.readInt(T, payload[0..@sizeOf(T)], .big); - } + // RLP integers are big-endian with leading zero bytes omitted, so the value + // may be shorter than @sizeOf(T) (readVarInt 0-extends) or empty (=> 0). A value + // wider than the target type is an overflow and rejected rather than truncated. + if (payload.len > @sizeOf(T)) return error.RlpIntOverflow; + out.* = std.mem.readVarInt(T, payload, .big); } // Returns the size of the payload as well as the offset to the // start of the actual data. -fn sizeAndDataOffset(payload: []const u8) !struct { size: usize, offset: usize } { +pub fn sizeAndDataOffset(payload: []const u8) !struct { size: usize, offset: usize } { var size: usize = undefined; var offset: usize = undefined; @@ -526,3 +516,32 @@ test "deserialize empty slice of non-byte types from short RLP list" { try std.testing.expect(consumed == rlp.len); try std.testing.expect(out.len == 0); } + +test "deserialize integer from empty RLP value (regression: no OOB panic)" { + // 0x80 is the RLP encoding of an empty byte string == integer 0. Its value + // region is zero-length, so decoding it into a u8 used to run + // `readInt(u8, payload[0..1])` on an empty slice and panic with + // "index out of bounds". It must instead 0-extend to 0. + { + const rlp = [_]u8{0x80}; + var out: u8 = 0xaa; + const consumed = try deserialize(u8, std.testing.allocator, &rlp, &out); + try std.testing.expectEqual(@as(usize, 1), consumed); + try std.testing.expectEqual(@as(u8, 0), out); + } + // 0xc0 is an empty list; its value region is also zero-length. This is the exact + // devp2p Disconnect `[]` payload that crashed a real decode. + { + const rlp = [_]u8{0xc0}; + var out: u8 = 0xaa; + const consumed = try deserialize(u8, std.testing.allocator, &rlp, &out); + try std.testing.expectEqual(@as(usize, 1), consumed); + try std.testing.expectEqual(@as(u8, 0), out); + } + // A value wider than the target type is rejected, not truncated or panicked. + { + const rlp = [_]u8{ 0x82, 0x03, 0xe8 }; // 2-byte string 0x03e8 + var out: u8 = 0; + try std.testing.expectError(error.RlpIntPayloadTooLong, deserialize(u8, std.testing.allocator, &rlp, &out)); + } +} diff --git a/src/serialize.zig b/src/serialize.zig index dcd640c..7160918 100644 --- a/src/serialize.zig +++ b/src/serialize.zig @@ -5,6 +5,7 @@ const Allocator = std.mem.Allocator; const hasFn = std.meta.hasFn; pub const deserialize = @import("deserialize.zig").deserialize; +const sizeAndDataOffset = @import("deserialize.zig").sizeAndDataOffset; fn writeLengthLength(length: usize, list: *ArrayList(u8)) !u8 { var enc_length_buf: [8]u8 = undefined; @@ -173,19 +174,27 @@ pub fn serialize(comptime T: type, allocator: Allocator, data: T, list: *ArrayLi } pub const RawRLPValue = union(enum) { - value: []const u8, - list: []const RawRLPValue, + value: []const u8, // already-encoded RLP; emitted verbatim + list: []const RawRLPValue, // children wrapped with an RLP list header pub fn encodeToRLP(self: RawRLPValue, allocator: Allocator, list: *ArrayList(u8)) !void { return switch (self) { .value => |v| { - try serialize([]const u8, allocator, v, list); + try list.appendSlice(v); }, .list => |v| { try serialize([]const RawRLPValue, allocator, v, list); }, }; } + + pub fn decodeFromRLP(self: *RawRLPValue, _: Allocator, serialized: []const u8) !usize { + const r = try sizeAndDataOffset(serialized); + const consumed = r.offset + r.size; + if (consumed > serialized.len) return error.RlpPayloadTooShort; + self.* = .{ .value = serialized[0..consumed] }; + return consumed; + } }; test "serialize an integer" { @@ -411,55 +420,50 @@ test "one byte slicei with value > 128" { try std.testing.expectEqualSlices(u8, &[_]u8{ 0x81, 0xff }, out.items); } -test "raw rlp" { +test "raw rlp value is verbatim, list wraps" { const allocator = std.testing.allocator; - { - var out_generic = ArrayList(u8).init(allocator); - defer out_generic.deinit(); - const generic: RawRLPValue = .{ .value = "hello" }; - try serialize(RawRLPValue, allocator, generic, &out_generic); - - var out = ArrayList(u8).init(allocator); - defer out.deinit(); - const normal = "hello"; - try serialize([]const u8, allocator, normal, &out); - - try std.testing.expectEqualSlices(u8, out.items, out_generic.items); - } - { - var out_generic = ArrayList(u8).init(allocator); - defer out_generic.deinit(); - const generic: RawRLPValue = .{ .list = &[_]RawRLPValue{ .{ .value = "hello" }, .{ .value = "world" } } }; - try serialize(RawRLPValue, allocator, generic, &out_generic); - - var out = ArrayList(u8).init(allocator); - defer out.deinit(); - var normal = [_][]const u8{ "hello", "world" }; - try serialize([]const []const u8, allocator, &normal, &out); - - try std.testing.expectEqualSlices(u8, out.items, out_generic.items); - } - { - var out_generic = ArrayList(u8).init(allocator); - defer out_generic.deinit(); - const generic: RawRLPValue = .{ - .list = &[_]RawRLPValue{ - .{ .value = "hello" }, - .{ .value = "world" }, - .{ .list = &[_]RawRLPValue{.{ .value = "nested" }} }, - }, - }; - try serialize(RawRLPValue, allocator, generic, &out_generic); - - var out = ArrayList(u8).init(allocator); - defer out.deinit(); - const normal: struct { a: []const u8, b: []const u8, c: []const []const u8 } = .{ - .a = "hello", - .b = "world", - .c = &[_][]const u8{"nested"}, - }; - try serialize(@TypeOf(normal), allocator, normal, &out); + var out = ArrayList(u8).init(allocator); + defer out.deinit(); - try std.testing.expectEqualSlices(u8, out.items, out_generic.items); - } + // .value holds already-encoded RLP and is emitted as-is. + try serialize(RawRLPValue, allocator, RawRLPValue{ .value = &([_]u8{0x85} ++ "hello".*) }, &out); + try testing.expectEqualSlices(u8, &([_]u8{0x85} ++ "hello".*), out.items); + + // .list wraps its raw children with a freshly computed list header. + out.clearRetainingCapacity(); + try serialize(RawRLPValue, allocator, RawRLPValue{ .list = &[_]RawRLPValue{ + .{ .value = &([_]u8{0x85} ++ "hello".*) }, + .{ .value = &([_]u8{0x85} ++ "world".*) }, + } }, &out); + try testing.expectEqualSlices(u8, &([_]u8{0xcc} ++ [_]u8{0x85} ++ "hello".* ++ [_]u8{0x85} ++ "world".*), out.items); +} + +test "raw rlp captures items verbatim" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + // [ "hello", "world", [ "nested" ] ] in canonical RLP. + const encoded = [_]u8{0xd4} ++ + [_]u8{0x85} ++ "hello".* ++ + [_]u8{0x85} ++ "world".* ++ + [_]u8{ 0xc7, 0x86 } ++ "nested".*; + + // As a single value: the whole item is captured verbatim (not split). + var whole: RawRLPValue = undefined; + try testing.expectEqual(encoded.len, try deserialize(RawRLPValue, a, &encoded, &whole)); + try testing.expectEqualSlices(u8, &encoded, whole.value); + + // As a slice: the list is split one level, each element captured verbatim + // (the nested list is NOT descended into). + var items: []RawRLPValue = undefined; + try testing.expectEqual(encoded.len, try deserialize([]RawRLPValue, a, &encoded, &items)); + try testing.expectEqual(@as(usize, 3), items.len); + try testing.expectEqualSlices(u8, &([_]u8{0x85} ++ "hello".*), items[0].value); + try testing.expectEqualSlices(u8, &([_]u8{ 0xc7, 0x86 } ++ "nested".*), items[2].value); + + // Re-encoding the elements reproduces the original list. + var reencoded = ArrayList(u8).init(a); + try serialize([]const RawRLPValue, a, items, &reencoded); + try testing.expectEqualSlices(u8, &encoded, reencoded.items); }