Repository navigation
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
7733d77 to
d0cfee6
Compare
This comment was marked as outdated.
This comment was marked as outdated.
7e4ce63 to
96323ce
Compare
This comment was marked as outdated.
This comment was marked as outdated.
6dff33e to
5742dda
Compare
|
AMP, pick and choose: Review of
|
103960a to
4a28a85
Compare
bettio
left a comment
There was a problem hiding this comment.
First review round. I didn't finish review yet but I start reporting the issues.
Found the following issues outside the changed files:
-
io_lib cannot format bitstrings. io_lib:format("~p", [<<1:1>>]). Same applies for `
w. -
binary:decode_hex/1returns silently wrong data for bitstrings;encode_hexaccepts them.
binary:decode_hex(<<"AB", 1:9>>) %% here: <<171>> OTP: error
binary:encode_hex(<<1:4>>) %% here: <<"1">> OTP: badarg - tests/erlang_tests/test_is_bitstring_is_binary.erl: I think we should update this file:
34 % bitstrings are currently unsupported
35 % true = is_bitstring(id(<<1:1>>)),
44 % bitstrings are currently unsupported
45 % false = is_binary(id(<<1:1>>)),
|
I think all comments addressed so far, plus some more things I noticed reviewing again on my end (e.g. difference with beam doc). |
511c7f4 to
e59a0de
Compare
bettio
left a comment
There was a problem hiding this comment.
We are getting close, there are still a few remaining issues.
Also let's add more tests (following are all falining):
C = ?MODULE:id($a),
F = ?MODULE:id(1.0),
<<176, 1:1>> = <<1:1, C/utf8>>,
<<22, 18>> = <<1:4, C/utf8, 2:4>>,
<<128, 48, 1:1>> = <<1:1, C/utf16>>,
<<128, 0, 0, 48, 1:1>> = <<1:1, C/utf32>>,
{97, <<>>} = (fun(<<_:1, X/utf8, R/bits>>) -> {X, R} end)(id(<<1:1, $a/utf8>>)),
{1, 97, 2} = (fun(<<A:4, X/utf8, B:4>>) -> {A, X, B} end)(id(<<1:4, $a/utf8, 2:4>>)),
97 = (fun(<<_:1, X/utf16>>) -> X end)(id(<<1:1, $a/utf16>>)),
97 = (fun(<<_:1, X/utf32>>) -> X end)(id(<<1:1, $a/utf32>>)),
<<159, 192, 0, 0, 0:1>> = <<1:1, F:32/float>>,
<<19, 248, 0, 0, 2>> = <<1:4, F:32/float, 2:4>>,
1.0 = (fun(<<_:1, V:32/float>>) -> V end)(id(<<1:1, 1.0:32/float>>)).There was a problem hiding this comment.
writes at offset >> 3 and drops the bit offset (same at :423 utf16, :457 utf32).
With unaligned offsets now allowed in bs_create_bin this silently clobbers the preceding bits: C = id($a), <<1:1, C/utf8>> gives <<97,0:1>>, BEAM <<176,1:1>>.
Idea: encode into a 4-byte stack buffer and bitstring_copy_bits(dst, offset, buf, size * 8); both interpreter and JIT go through these helpers.
There was a problem hiding this comment.
decodes from offset >> 3: <<:1, X/utf8>> = id(<<1:1, $a/utf8>>) is a badmatch (BEAM: 97) and <<:1, X/utf16>> = id(<<1:1, $a/utf16>>) returns 32816.
Idea: when offset & 7, copy up to 4 bytes (bounded by the remaining bits) with bitstring_copy_bits_from into a zeroed temp buffer and decode from that.
See also line 438 and line 471.
| result = bitstring_insert_f64(t, offset, float_value, flags_value); | ||
| } | ||
| if (UNLIKELY(!result)) { | ||
| TRACE("bs_create_bin/6: Failed to insert float into binary\n"); |
There was a problem hiding this comment.
A float segment at an unaligned offset ends in badarg here (and at line 3812), but F = id(1.0), <<1:1, F:32/float>> is valid and BEAM builds <<159,192,0,0,0:1>>.
This issue was already existing, but now reachable for every bitstring.
idea: IEEE bits are an integer, so bitstring_insert_any_integer already does the unaligned write for 16/32/64 bits in both endiannesses.
Let's address this on an additional commit since it is not an issue introduced from this PR.
| bool status; | ||
| switch (size_val) { | ||
| case 16: | ||
| status = bitstring_extract_f16(bs_bin, bs_offset, increment, flags_value, &value); |
There was a problem hiding this comment.
Same for extraction: bitstring_extract_f16/f32/f64 return false at an unaligned offset, so case id(<<1:1, 1.0:32/float>>) of <<_:1, F:32/float>> -> F; _ -> nomatch end silently returns nomatch (BEAM: 1.0).
This PR changed the integer case at line 4501 to raise unsupported instead of silently failing; floats should follow, or extract the bits with bitstring_extract_any_integer and reinterpret.
| } else if ((offset % 8 != 0) || (n % 8 != 0)) { | ||
| // TODO: intn_to_integer_bytes writes whole bytes only, so a bignum | ||
| // segment at a bit offset needs a bit-level writer. | ||
| return false; |
There was a problem hiding this comment.
Returning false ends in badarg, while the interpreter raises unsupported for the same unaligned >64-bit segment.
verified on the x86_64 JIT build with N = id(1 bsl 70), <<1:1, N:71>>.
Also tighten the unsupported | badarg acceptance in test_no_bs_create_bin.erl.
| memcpy(dst, bin, binary_size); | ||
| return binary_size * 8; | ||
| bitstring_copy_bits(dst, offset, bin, copy_bits); | ||
| return (int) copy_bits; |
There was a problem hiding this comment.
I would rather suggest using size_t here in this function.
| } | ||
| } | ||
|
|
||
| void bitstring_copy_bits_from(uint8_t *dst, const uint8_t *src, size_t src_offset, size_t bits_count) |
There was a problem hiding this comment.
Bit-by-bit loop: <<_:1, Rest/bits>> on a 1 MiB binary runs 8M iterations. A byte loop (src[i] << s) | (src[i + 1] >> (8 - s)) is about 8x faster; the unaligned branch of bitstring_copy_bits_incomplete_bytes (line 352) has the same shape.
|
|
||
| term bitstring_slice(term bs_bin, size_t offset, size_t len_bits, Heap *heap, GlobalContext *glb) | ||
| { | ||
| if (offset % 8 == 0 && len_bits % 8 == 0) { |
There was a problem hiding this comment.
This copies whenever len_bits is not a multiple of 8, even when offset is byte-aligned, so a bitstring with trailing bits can never share storage: every <<_:8, Rest/bits>> or sized slice materializes the whole remainder, bit by bit.
Since a peer can turn any payload into a bitstring (BIT_BINARY_EXT instead of BINARY_EXT, or one extra bit on the wire), linear parsers become quadratic on attacker-controlled input. When offset % 8 == 0, share the source as split_binary/2 does at nifs.c:3773, under the same refc/SUB_BINARY_MIN policy as term_maybe_create_sub_binary.
|
Also one more issue in opcodesswitch.h: a value in [2^63, 2^64) is a bignum here, so it takes the term_to_bigint path below and a non-byte-aligned segment ends in unsupported at line 5972 (badarg on the JIT, same dispatch at jit.c:1700). That makes <<Flag:1, Hash:64>> fail for half of all 64-bit hashes: H = id(16#9E3779B97F4A7C15), <<1:1, H:64>> raises on both runtimes while BEAM builds <<207,27,188,220,191,165,62,10,1:1>>; matching the same field back (<<_:1, X:64>>) already works. differences-with-beam.md:167 promises that integers fitting in 64 bits have no restriction. Idea when big_sign is positive, intn_is_uint64(big_src_value, big_len) holds and the field is at most 64 bits, call bitstring_insert_integer with (avm_int64_t) intn_to_uint64(big_src_value); the low 64 bits of the two's-complement pattern are exactly the field contents, and the 64-bit limit keeps the sign-fill of wider fields (which assumes a signed value) out of the picture. Mirror it in jit_bitstring_insert_integer. |
fe5730c to
1d46dba
Compare
…etup After a CALLX8, call_func_ptr picked the result register among a5-a7 and then a8-a15, so with a5-a7 in use the result went to a8. But a8 is the scratch register, not an allocatable one: the next if_block merge dropped it from the used registers, so a following call did not save it, and any instruction using the scratch register could overwrite it. Building <<E/little-utf16, E/utf8>> lost the offset of the second segment this way and wrote it out of bounds. Pick the result among the allocatable registers, keeping a8 as the last resort when all of them are in use. When the function pointer is in a parameter register and no register outside them is free, call_func_ptr moved it to a register an argument lived in, passing that argument through a freed parameter register instead. The argument could still be live after the call, and as a5-a7 are neither saved nor restored around a call, its value was lost. Building <<Imm:32/little>> for a boxed Imm in jit_x86_64_asm lost the binary this way once results stopped going to a8. Move the function pointer to the scratch register instead, as it holds no argument and the call clobbers it anyway. Signed-off-by: Paul Guyot <pguyot@kallisys.net>
Also fix iolist and chardata functions accepting an integer improper tail. Signed-off-by: Paul Guyot <pguyot@kallisys.net>
Float segments were only written and read at byte-aligned offsets: elsewhere construction raised badarg and matching failed, so `<<1:1, F:32/float>>` could not be built and `<<_:1, F:32/float>>` never matched. Write and read their IEEE bits with the integer helpers, which handle any bit offset in both byte orders and check the bounds against the bit size. Also have the JIT decode the size of a float segment first, as it does for integer segments: on x86_64, a variable size was decoded once the value and the flags held registers, and the badarg check then found none left. Signed-off-by: Paul Guyot <pguyot@kallisys.net>
Continuation of:
These changes are made under both the "Apache 2.0" and the "GNU Lesser General
Public License 2.1 or later" license terms (dual license).
SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later