diff --git a/.github/workflows/build-and-test-macos.yaml b/.github/workflows/build-and-test-macos.yaml index 6738be264a..1d18a21030 100644 --- a/.github/workflows/build-and-test-macos.yaml +++ b/.github/workflows/build-and-test-macos.yaml @@ -254,6 +254,11 @@ jobs: run: | ./tests/test-structs + - name: "Test: test-term" + working-directory: build + run: | + ./tests/test-term + - name: "Test: test_etest.avm" timeout-minutes: 5 working-directory: build diff --git a/.github/workflows/build-and-test-on-freebsd.yaml b/.github/workflows/build-and-test-on-freebsd.yaml index f22718ecc2..d7a3b49407 100644 --- a/.github/workflows/build-and-test-on-freebsd.yaml +++ b/.github/workflows/build-and-test-on-freebsd.yaml @@ -157,6 +157,13 @@ jobs: cd build ./tests/test-structs + - name: "Test: test-term" + shell: freebsd {0} + run: | + cd $GITHUB_WORKSPACE; + cd build + ./tests/test-term + - name: "Test: test_etest.avm" shell: freebsd {0} run: | diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index f487ea0cf3..48de1aa824 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -875,6 +875,19 @@ jobs: ulimit -c unlimited ./tests/test-structs + - name: "Test: test-term with valgrind" + if: matrix.library-arch == '' + working-directory: build + run: | + ulimit -c unlimited + valgrind --error-exitcode=1 ./tests/test-term + + - name: "Test: test-term" + working-directory: build + run: | + ulimit -c unlimited + ./tests/test-term + - name: "Test: test_etest.avm with valgrind" if: matrix.library-arch == '' timeout-minutes: 5 diff --git a/.github/workflows/build-linux-artifacts.yaml b/.github/workflows/build-linux-artifacts.yaml index ff9018cc70..cd65b9cc87 100644 --- a/.github/workflows/build-linux-artifacts.yaml +++ b/.github/workflows/build-linux-artifacts.yaml @@ -236,6 +236,7 @@ jobs: make test-heap && make test-mailbox && make test-structs && + make test-term && file ./tests/test-erlang && ./tests/test-erlang -s prime_smp && file ./tests/test-enif && @@ -246,6 +247,8 @@ jobs: ./tests/test-mailbox && file ./tests/test-structs && ./tests/test-structs && + file ./tests/test-term && + ./tests/test-term && file ./src/AtomVM && ./src/AtomVM tests/libs/etest/test_etest.avm && ./src/AtomVM tests/libs/estdlib/test_estdlib.avm && diff --git a/CHANGELOG.md b/CHANGELOG.md index ecedd437fc..d0b1717b0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug where negative or oversized segment sizes were not rejected in binary matching - Fixed the `network` mdns configuration to read the documented `host` key; the previously required, undocumented `hostname` key is still accepted +- Fixed `term_is_uint32` accepting big integers whose low 64 bits are within range on 32-bit + builds, which made `erlang:crc32/2`, `erlang:crc32_combine/3` and `crypto:pbkdf2_hmac/5` + silently truncate huge integer arguments instead of raising `badarg` ## [0.7.0-alpha.1] - 2026-04-06 diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index 40e943b415..46038f6a1e 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -1746,7 +1746,7 @@ static inline bool term_is_uint32(term t) #endif #if BOXED_TERMS_REQUIRED_FOR_INT != BOXED_TERMS_REQUIRED_FOR_INT64 - } else { + } else if (term_boxed_size(t) == BOXED_TERMS_REQUIRED_FOR_INT64) { avm_int64_t unboxed64 = term_unbox_int64(t); return unboxed64 <= UINT32_MAX; #endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3c7f880c49..2ba0944bab 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -27,6 +27,7 @@ add_executable(test-heap test-heap.c) add_executable(test-jit_stream_flash test-jit_stream_flash.c ../src/libAtomVM/jit_stream_flash.c) add_executable(test-mailbox test-mailbox.c) add_executable(test-structs test-structs.c) +add_executable(test-term test-term.c) target_compile_features(test-erlang PUBLIC c_std_11) target_compile_features(test-enif PUBLIC c_std_11) @@ -34,6 +35,7 @@ target_compile_features(test-heap PUBLIC c_std_11) target_compile_features(test-jit_stream_flash PUBLIC c_std_11) target_compile_features(test-mailbox PUBLIC c_std_11) target_compile_features(test-structs PUBLIC c_std_11) +target_compile_features(test-term PUBLIC c_std_11) if(CMAKE_COMPILER_IS_GNUCC) target_compile_options(test-erlang PUBLIC -Wall -pedantic -Wextra -ggdb) @@ -42,6 +44,7 @@ if(CMAKE_COMPILER_IS_GNUCC) target_compile_options(test-jit_stream_flash PUBLIC -Wall -pedantic -Wextra -ggdb) target_compile_options(test-mailbox PUBLIC -Wall -pedantic -Wextra -ggdb) target_compile_options(test-structs PUBLIC -Wall -pedantic -Wextra -ggdb) + target_compile_options(test-term PUBLIC -Wall -pedantic -Wextra -ggdb) endif() if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") @@ -56,6 +59,7 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") target_link_libraries(test-jit_stream_flash PRIVATE ${LIBRT}) target_link_libraries(test-mailbox PRIVATE ${LIBRT}) target_link_libraries(test-structs PRIVATE ${LIBRT}) + target_link_libraries(test-term PRIVATE ${LIBRT}) else() # might also be in libc check_library_exists(c clock_gettime "" HAVE_CLOCK_GETTIME) @@ -70,6 +74,7 @@ if (MbedTLS_FOUND) target_link_libraries(test-jit_stream_flash PRIVATE MbedTLS::mbedtls) target_link_libraries(test-mailbox PRIVATE MbedTLS::mbedtls) target_link_libraries(test-structs PRIVATE MbedTLS::mbedtls) + target_link_libraries(test-term PRIVATE MbedTLS::mbedtls) endif() set( @@ -87,6 +92,7 @@ if((${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") OR target_include_directories(test-jit_stream_flash PRIVATE ../src/platforms/generic_unix/lib) target_include_directories(test-mailbox PRIVATE ../src/platforms/generic_unix/lib) target_include_directories(test-structs PRIVATE ../src/platforms/generic_unix/lib) + target_include_directories(test-term PRIVATE ../src/platforms/generic_unix/lib) else() message(FATAL_ERROR "Unsupported platform: ${CMAKE_SYSTEM_NAME}") endif() @@ -97,6 +103,7 @@ target_include_directories(test-heap PRIVATE ../src/libAtomVM) target_include_directories(test-jit_stream_flash PRIVATE ../src/libAtomVM ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(test-mailbox PRIVATE ../src/libAtomVM) target_include_directories(test-structs PRIVATE ../src/libAtomVM) +target_include_directories(test-term PRIVATE ../src/libAtomVM) target_link_libraries(test-erlang PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-enif PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-heap PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) @@ -108,6 +115,7 @@ endif() target_link_libraries(test-jit_stream_flash PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-mailbox PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) target_link_libraries(test-structs PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) +target_link_libraries(test-term PRIVATE libAtomVM libAtomVM${PLATFORM_LIB_SUFFIX}) # Except for XCode, also compile beams if (NOT "${CMAKE_GENERATOR}" MATCHES "Xcode") @@ -133,12 +141,14 @@ if (COVERAGE) append_coverage_compiler_flags_to_target(test-jit_stream_flash) append_coverage_compiler_flags_to_target(test-mailbox) append_coverage_compiler_flags_to_target(test-structs) + append_coverage_compiler_flags_to_target(test-term) append_coverage_linker_flags_to_target(test-erlang) append_coverage_linker_flags_to_target(test-enif) append_coverage_linker_flags_to_target(test-heap) append_coverage_linker_flags_to_target(test-jit_stream_flash) append_coverage_linker_flags_to_target(test-mailbox) append_coverage_linker_flags_to_target(test-structs) + append_coverage_linker_flags_to_target(test-term) if (CMAKE_COMPILER_IS_GNUCC) setup_target_for_coverage_lcov(NAME coverage EXECUTABLE test-erlang DEPENDENCIES test-erlang) endif() diff --git a/tests/erlang_tests/test_crc32.erl b/tests/erlang_tests/test_crc32.erl index 4b973ea374..3841495f50 100644 --- a/tests/erlang_tests/test_crc32.erl +++ b/tests/erlang_tests/test_crc32.erl @@ -110,6 +110,15 @@ test_crc32_badarg() -> ok = expect_badarg(fun() -> erlang:crc32_combine(0, 0, ?MODULE:id(-1)) end), ok = expect_badarg(fun() -> erlang:crc32_combine(?MODULE:id(16#100000000), 0, 0) end), ok = expect_badarg(fun() -> erlang:crc32_combine(0, ?MODULE:id(16#100000000), 0) end), + + % big integers whose low 64 bits are small must not pass the uint32 check + ok = expect_badarg(fun() -> erlang:crc32(?MODULE:id(1 bsl 64), <<"data">>) end), + ok = expect_badarg(fun() -> erlang:crc32(?MODULE:id((1 bsl 64) + 7), <<"data">>) end), + ok = expect_badarg(fun() -> erlang:crc32(?MODULE:id(-(1 bsl 64)), <<"data">>) end), + ok = expect_badarg(fun() -> erlang:crc32(?MODULE:id(1 bsl 130), <<"data">>) end), + ok = expect_badarg(fun() -> erlang:crc32_combine(?MODULE:id(1 bsl 64), 0, 0) end), + ok = expect_badarg(fun() -> erlang:crc32_combine(0, ?MODULE:id((1 bsl 64) + 7), 0) end), + ok = expect_badarg(fun() -> erlang:crc32_combine(0, 0, ?MODULE:id(1 bsl 64)) end), ok. test_crc32_boundary() -> diff --git a/tests/test-term.c b/tests/test-term.c new file mode 100644 index 0000000000..53d2021161 --- /dev/null +++ b/tests/test-term.c @@ -0,0 +1,272 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Davide Bettio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include +#include +#include +#include + +#include "context.h" +#include "globalcontext.h" +#include "intn.h" +#include "memory.h" +#include "term.h" +#include "utils.h" + +// Enough room for all terms created by a single test function, so that no GC +// runs between term creation and the checks +#define TEST_HEAP_SIZE 256 + +enum +{ + IsIntegerOn32Bit = 1 << 0, + IsIntegerOn64Bit = 1 << 1, + IsPositive = 1 << 2, + IsNegative = 1 << 3, + IsUint8 = 1 << 4, + IsInt32 = 1 << 5, + IsUint32 = 1 << 6, + IsInt64 = 1 << 7, + IsUint64 = 1 << 8, + IsBigint = 1 << 9 +}; + +static bool flags_is_immediate(unsigned int flags) +{ +#if TERM_BITS == 32 + return (flags & IsIntegerOn32Bit) != 0; +#elif TERM_BITS == 64 + return (flags & IsIntegerOn64Bit) != 0; +#else +#error "Unsupported TERM_BITS" +#endif +} + +static void assert_integer_predicates(term t, unsigned int flags) +{ + bool is_immediate = flags_is_immediate(flags); + bool is_positive = (flags & IsPositive) != 0; + bool is_negative = (flags & IsNegative) != 0; + + assert(term_is_any_integer(t)); + assert(term_is_number(t)); + assert(term_is_int(t) == is_immediate); + assert(term_is_boxed_integer(t) == !is_immediate); + + assert(term_is_non_neg_int(t) == (is_immediate && !is_negative)); + assert(term_is_pos_int(t) == (is_immediate && is_positive)); + assert(term_is_neg_int(t) == (is_immediate && is_negative)); + + assert(term_is_pos_boxed_integer(t) == (!is_immediate && !is_negative)); + assert(term_is_neg_boxed_integer(t) == (!is_immediate && is_negative)); + if (!is_immediate) { + assert(term_boxed_integer_sign(t) + == (is_negative ? TermNegativeInteger : TermPositiveInteger)); + } + + assert(term_is_any_non_neg_integer(t) == !is_negative); + assert(term_is_any_pos_integer(t) == is_positive); + assert(term_is_any_neg_integer(t) == is_negative); + + assert(term_is_bigint(t) == ((flags & IsBigint) != 0)); +} + +static term make_bigint_term( + Context *ctx, const intn_digit_t *digits, size_t len, term_integer_sign_t sign) +{ + size_t count = intn_count_digits(digits, len); + size_t intn_data_size; + size_t rounded_num_len; + term_bigint_size_requirements(count, &intn_data_size, &rounded_num_len); + + term t = term_create_uninitialized_bigint(intn_data_size, sign, &ctx->heap); + term_initialize_bigint(t, digits, count, rounded_num_len); + return t; +} + +static void test_int64_range_checks(void) +{ + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + enum MemoryGCResult res = memory_ensure_free(ctx, TEST_HEAP_SIZE); + assert(res == MEMORY_GC_OK); + + static const struct + { + int64_t value; + unsigned int flags; + } cases[] = { + { 0, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsUint8 | IsInt32 | IsUint32 | IsInt64 + | IsUint64 }, + { 1, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsUint8 | IsInt32 | IsUint32 + | IsInt64 | IsUint64 }, + { -1, IsIntegerOn32Bit | IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { 42, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsUint8 | IsInt32 | IsUint32 + | IsInt64 | IsUint64 }, + { -42, IsIntegerOn32Bit | IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { 255, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsUint8 | IsInt32 | IsUint32 + | IsInt64 | IsUint64 }, + { 256, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsInt32 | IsUint32 | IsInt64 + | IsUint64 }, + // 2^27 - 1 and -2^27: immediate boundary on 32-bit builds + { 0x07FFFFFF, + IsIntegerOn32Bit | IsIntegerOn64Bit | IsPositive | IsInt32 | IsUint32 | IsInt64 + | IsUint64 }, + { 0x08000000, IsIntegerOn64Bit | IsPositive | IsInt32 | IsUint32 | IsInt64 | IsUint64 }, + { -0x08000000, IsIntegerOn32Bit | IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { -0x08000001, IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { INT32_MAX, IsIntegerOn64Bit | IsPositive | IsInt32 | IsUint32 | IsInt64 | IsUint64 }, + { INT32_MIN, IsIntegerOn64Bit | IsNegative | IsInt32 | IsInt64 }, + { (int64_t) INT32_MAX + 1, IsIntegerOn64Bit | IsPositive | IsUint32 | IsInt64 | IsUint64 }, + { (int64_t) INT32_MIN - 1, IsIntegerOn64Bit | IsNegative | IsInt64 }, + { UINT32_MAX, IsIntegerOn64Bit | IsPositive | IsUint32 | IsInt64 | IsUint64 }, + { (int64_t) UINT32_MAX + 1, IsIntegerOn64Bit | IsPositive | IsInt64 | IsUint64 }, + // 2^59 - 1 and -2^59: immediate boundary on 64-bit builds + { 0x07FFFFFFFFFFFFFF, IsIntegerOn64Bit | IsPositive | IsInt64 | IsUint64 }, + { 0x0800000000000000, IsPositive | IsInt64 | IsUint64 }, + { -0x0800000000000000, IsIntegerOn64Bit | IsNegative | IsInt64 }, + { -0x0800000000000001, IsNegative | IsInt64 }, + { INT64_MAX, IsPositive | IsInt64 | IsUint64 }, + { INT64_MIN, IsNegative | IsInt64 }, + }; + + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + int64_t value = cases[i].value; + unsigned int flags = cases[i].flags; + term t = term_make_maybe_boxed_int64(value, &ctx->heap); + + assert_integer_predicates(t, flags); + + if (flags_is_immediate(flags)) { + assert(term_is_integer(t) == true); + assert(term_to_int(t) == (avm_int_t) value); + } else { + assert(term_is_integer(t) == false); + } + if (flags & IsUint8) { + assert(term_is_uint8(t) == true); + assert(term_to_uint8(t) == (uint8_t) value); + } else { + assert(term_is_uint8(t) == false); + } + if (flags & IsInt32) { + assert(term_is_int32(t) == true); + assert(term_to_int32(t) == (int32_t) value); + } else { + assert(term_is_int32(t) == false); + } + if (flags & IsUint32) { + assert(term_is_uint32(t) == true); + assert(term_to_uint32(t) == (uint32_t) value); + } else { + assert(term_is_uint32(t) == false); + } + if (flags & IsInt64) { + assert(term_is_int64(t) == true); + assert(term_to_int64(t) == value); + } else { + assert(term_is_int64(t) == false); + } + if (flags & IsUint64) { + assert(term_is_uint64(t) == true); + assert(term_to_uint64(t) == (uint64_t) value); + } else { + assert(term_is_uint64(t) == false); + } + } + + context_destroy(ctx); + globalcontext_destroy(glb); +} + +static void test_bigint_range_checks(void) +{ + GlobalContext *glb = globalcontext_new(); + Context *ctx = context_new(glb); + + enum MemoryGCResult res = memory_ensure_free(ctx, TEST_HEAP_SIZE); + assert(res == MEMORY_GC_OK); + + static const struct + { + intn_digit_t digits[INTN_MAX_IN_LEN]; + size_t len; + unsigned int flags; + uint64_t uint64_value; + } cases[] = { + // 2^63 + { { 0x00000000, 0x80000000 }, 2, IsPositive | IsBigint | IsUint64, UINT64_C(1) << 63 }, + // 2^64 - 1 + { { 0xFFFFFFFF, 0xFFFFFFFF }, 2, IsPositive | IsBigint | IsUint64, UINT64_MAX }, + // 2^64: low 64 bits are 0 + { { 0x00000000, 0x00000000, 0x00000001 }, 3, IsPositive | IsBigint, 0 }, + // 2^64 + 7: low 64 bits are 7 + { { 0x00000007, 0x00000000, 0x00000001 }, 3, IsPositive | IsBigint, 0 }, + // 2^65 + 2^40 + { { 0x00000000, 0x00000100, 0x00000002 }, 3, IsPositive | IsBigint, 0 }, + // -(2^63 + 1) + { { 0x00000001, 0x80000000 }, 2, IsNegative | IsBigint, 0 }, + // -(2^64) + { { 0x00000000, 0x00000000, 0x00000001 }, 3, IsNegative | IsBigint, 0 }, + // 2^256 - 1: largest supported magnitude + { { 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, + 0xFFFFFFFF }, + 8, IsPositive | IsBigint, 0 }, + }; + + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + unsigned int flags = cases[i].flags; + term_integer_sign_t sign = (flags & IsNegative) ? TermNegativeInteger : TermPositiveInteger; + term t = make_bigint_term(ctx, cases[i].digits, cases[i].len, sign); + + assert_integer_predicates(t, flags); + + assert(term_is_uint8(t) == false); + assert(term_is_int32(t) == false); + assert(term_is_uint32(t) == false); + assert(term_is_int64(t) == false); + if (flags & IsUint64) { + assert(term_is_uint64(t) == true); + assert(term_to_uint64(t) == cases[i].uint64_value); + } else { + assert(term_is_uint64(t) == false); + } + } + + context_destroy(ctx); + globalcontext_destroy(glb); +} + +int main(int argc, char **argv) +{ + UNUSED(argc); + UNUSED(argv); + + test_int64_range_checks(); + test_bigint_range_checks(); + + return EXIT_SUCCESS; +}