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). |
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. |
1d46dba to
c3ed748
Compare
bettio
left a comment
There was a problem hiding this comment.
Found a couple of issues with older opcodes (so basically they are hard to reach). If it doesn't take too much time, do you mind fixing them?
Otherwise I can accept this PR and take care of this issue later.
| size_bits = (size_t) signed_size_value * unit; | ||
| } else if (size == ALL_ATOM) { | ||
| size_val = term_binary_size(src); | ||
| size_bits = src_bits; |
There was a problem hiding this comment.
This line must change because VERIFY_IS_BITSTRING now lets a bitstring source through, but the all branch sets size_bits = src_bits without the src_bits % unit check that this PR added to bs_append and bs_private_append.
For a /binary segment BEAM raises badarg; AtomVM now silently builds a result.
Possible fix:
} else if (size == ALL_ATOM) {
if (UNLIKELY(unit == 0 || (src_bits % unit) != 0)) {
RAISE_ERROR(BADARG_ATOM);
}
size_bits = src_bits;with +no_bs_create_bin +no_bs_match +no_type_opt:
f(B) -> <<1:1, B/binary>>. % f(<<1:1>>) OTP 27: badarg AtomVM: <<192,0>>
h(A, B) -> <<A/bitstring, B/binary>>. % h(<<5:3>>, <<7:3>>) OTP 27: badarg AtomVM: <<188,0:3>>
| if (size_val % 8 != 0) { | ||
| TRACE("bs_init_bits: size_val (" AVM_INT_FMT ") is not evenly divisible by 8\n", size_val); | ||
| RAISE_ERROR(UNSUPPORTED_ATOM); | ||
| if (UNLIKELY(size_val < 0)) { |
There was a problem hiding this comment.
This check was the only thing stopping the size that bs_add computes a few lines above: OP_BS_ADD returns (src1 + src2) * unit, while BEAM defines bs_add as src1 + src2 * unit. With the alignment check gone, the legacy opcodes now allocate that wrong total and silently build wrong-size binaries.
%% compiled with OTP 27 +no_bs_create_bin
f(B) -> <<1:1, B/binary>>. % f(<<1,2>>) OTP 27: <<128,129,0:1>> AtomVM: <<128,129,0>>The compiler emits bs_add(0, 1, 1) then bs_add(1, byte_size(B), 8); AtomVM computes 24 bits instead of 17 and returns <<128,129,0>>, OTP 27 returns <<128,129,0:1>>. Possible fix at 3425:
WRITE_REGISTER(dreg, term_from_int(src1_val + src2_val * unit));| %% a8 is the scratch register and is not allocatable: a result left there | ||
| %% would be dropped from the bookkeeping by the next merge and could be | ||
| %% overwritten, so it is only the last resort below. | ||
| PostCallAvail = (AvailableRegs1 -- HighRegs) ++ (HighRegs -- [?A8_REG | RegsToSave]), |
There was a problem hiding this comment.
Agree with keeping results out of a8. One thing I noticed, in case it's not expected: with results now in a9–a15, set_registers_args0 can run out of parking registers and fail with {badmatch,[]} at line 2113. Smallest example:
f(_, C) -> <<C/utf16-little, C/utf32-little>>.
There was a problem hiding this comment.
Checked against the JIT compiler at the base commit (18b3587): it compiles this, the PR does not, so it is a regression of this commit rather than a pre-existing limit.
Trigger is two little-endian utf segments in one bs_create_bin:
n(C) -> <<C/utf16-little, C/utf32-little>>. % base: ok, PR: {badmatch,[]}
whereas <<A/utf8, C/utf16-little>>, <<V:N, C/utf32-little>>, <<A/utf8, C/utf16>>, <<A/utf16, C/utf32>>, <<A/utf16-little, C:8>> and <<A:8, C/utf32-little>> compile on both.
Frames: set_registers_args0 (jit_xtensa.erl:2113) <- set_registers_args (:2008) <- call_func_ptr (:1906) <- the utf32 insert clause (jit.erl:2836). With the utf16 result now parked in a9-a15 instead of a8, AvailableScratchGP is empty when the utf32 call has to move an argument out of a parameter register, and [Avail | AvailGPT] = AvailGP fails. Either give set_registers_args0 a fallback when AvailGP is empty (spill, or a8 when the function pointer is not there) or keep this change but handle that case before merging, since an ESP32 module with that construct no longer JIT-compiles.
| } else if (term_is_external_reference(t)) { | ||
| return hash_external_reference(t, h, global); | ||
| } else if (term_is_binary(t)) { | ||
| } else if (term_is_bitstring(t)) { |
There was a problem hiding this comment.
Not a correctness problem, lookups still work because the key comparison runs after the hash.
hash_binary folds term_binary_size(t) whole bytes only, so <<0:1>>, <<1:1>>, <<7:3>> and any
two bitstrings that differ only in their partial byte land in the same bucket. Since this hunk
is what makes bitstrings hashable, consider folding term_bit_size(t) and the masked last byte
in as well, here or in a follow-up commit.
Also a TODO for the future might be a good option too.
|
|
||
| Integers from -2^63 to 2^63 - 1 have no such restriction and may be used at any offset and size. | ||
| Integers from 2^63 to 2^64 - 1 may be used at any offset in segments of up to 64 bits, such as | ||
| `<<Flag:1, Hash:64>>`. |
There was a problem hiding this comment.
Actually, offset 0 fails, every byte-aligned offset fails the same way, and when a segment follows, the failure turns into silently wrong bytes, which is the #2443 behaviour. Non-byte-aligned offsets work.
We might fix it, see my comment in opcodesswitch.h:5981
| RAISE_ERROR(BADARG_ATOM); | ||
| } | ||
| } else if ((offset % 8 != 0) || ((size_value * segment_unit) % 8 != 0)) { | ||
| // intn_to_integer_bytes writes whole bytes only, so a bignum |
There was a problem hiding this comment.
This condition sends a bignum through bitstring_bigint_to_field64 only when the offset or the
size is not a multiple of 8.
A byte-aligned field still goes to intn_to_integer_bytes with avail, so <<H:32>> and <<7:8, H:32>> raise badarg and <<7:8, H:32, 0:32>> returns wrong bytes, for H = id(16764730074108805316), while <<1:1, H:32>> is correct.
This is the #2443 behaviour, and I think the same issue exists at line 3681 and jit.c:1713.
|
About #2444: fixing it in this PR looks like the easiest option, since it touches the same code. Otherwise postponing it is fine, I left the issue open on purpose as a reminder. |
3127ebc to
dc733ab
Compare
bettio
left a comment
There was a problem hiding this comment.
It seems that all 32-bit builds are failing on CI:
test_bs_bigint_unaligned:
CRASH
======
pid: <0.1.0>
Stacktrace:
[{test_bs_bigint_unaligned,bi_sweep_values,3,[{file,"/home/runner/work/AtomVM/AtomVM/tests/erlang_tests/test_bs_bigint_common.hrl"},{line,406}]},{test_bs_bigint_unaligned,bi_sweep_sizes,2,[{file,"/home/runner/work/AtomVM/AtomVM/tests/erlang_tests/test_bs_bigint_common.hrl"},{line,392}]},{test_bs_bigint_unaligned,bi_sweep_prefixes,1,[{file,"/home/runner/work/AtomVM/AtomVM/tests/erlang_tests/test_bs_bigint_common.hrl"},{line,386}]},{test_bs_bigint_unaligned,bigint_test,0,[{file,"/home/runner/work/AtomVM/AtomVM/tests/erlang_tests/test_bs_bigint_common.hrl"},{line,31}]},{test_bs_bigint_unaligned,start,0,[{file,"/home/runner/work/AtomVM/AtomVM/tests/erlang_tests/test_bs_bigint_unaligned.erl"},{line,29}]}]
cp: #CP<module: 0, label: 42, offset: 2048>
x[0]: error
x[1]: {badmatch,<<25:5>>}
x[2]: {5,5,155,2,[{0,43490},{0,1532},{0,13536},{0,13932},{0,17538}],error}
Stack
-----
[]
[]
<<9:5>>
<<9:5>>
0
-9223372036854775809
[18446744073709551616,1180591620717411315769,-1180591620717411315769,1606938044258990275541962092341162602522202993782792835301383,-1606938044258990275541962092341162602522202993782792835301385,57896044618658097711785492504343953926634992332820282019728792003956564819968,-57896044618658097711785492504343953926634992332820282019728792003956564819968,115792089237316195423570985008687907853269984665640564039457584007913129639935,-115792089237316195423570985008687907853269984665640564039457584007913129639935]
2
0
#CP<module: 0, label: 39, offset: 264>
[3,7,8,9,15,16,17,31,32,33,63,64,65,66,71,72,73,100,127,128,129,200,255]
2
0
#CP<module: 0, label: 36, offset: 258>
[1,3,4,7,8,9,15]
#CP<module: 0, label: 4, offset: 362>
#CP<module: 0, label: 125, offset: 100>
#CP<module: 0, label: 0, offset: 1056>
Mailbox
-------
Monitors
--------
**End Of Crash Report**
Expected 0 but result is not an integer
test_bs_bigint_unaligned:FAILED
…etup Call results could land in a8, the scratch register, and the function pointer could be moved over a live argument. Keep results in allocatable registers, move the function pointer to a8, and order argument moves without a temporary when no register is left to park one. Signed-off-by: Paul Guyot <pguyot@kallisys.net>
Also fix iolist and chardata functions accepting an integer improper tail. Fixes atomvm#2444 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>
Byte-aligned bignums were written over the rest of the binary, and non-byte-aligned ones were limited to 64 bits. Fixes atomvm#2443 Signed-off-by: Paul Guyot <pguyot@kallisys.net>
bs_add computed (Src1 + Src2) * Unit instead of Src1 + Src2 * Unit, and bs_init_bits and bs_append failed to load with an allocation list. Signed-off-by: Paul Guyot <pguyot@kallisys.net>
The stm32h562rgt6 firmware no longer fit in 512 KB. This saves about 26 KB. Signed-off-by: Paul Guyot <pguyot@kallisys.net>
A bignum of 3 terms was taken for an int64 when combined with a small integer. 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