From 91e96d9d234c3d2456e920268859d482ab95422e Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sun, 15 Feb 2026 23:56:51 +0100 Subject: [PATCH] Add CodeQL query to check for allocations not preceeded by ensure_free Fix several cases where this happened in nifs. Also add a NOLINT comment for cases where the query is not smart enough to remove the couple of false positives. Also clean up some CodeQL logic and fixed a cleanup in dac_driver.c if allocation failed Signed-off-by: Paul Guyot --- .github/workflows/codeql-analysis.yaml | 2 +- .github/workflows/esp32-build.yaml | 2 +- .github/workflows/pico-build.yaml | 2 +- .github/workflows/stm32-build.yaml | 4 +- .github/workflows/wasm-build.yaml | 2 +- .../allocations-exceeding-ensure-free.ql | 120 ++++++--- .../allocations-without-ensure-free.ql | 248 ++++++++++++++++++ src/libAtomVM/nifs.c | 8 +- .../components/avm_builtins/dac_driver.c | 2 + .../esp32/components/avm_sys/platform_nifs.c | 13 +- 10 files changed, 355 insertions(+), 48 deletions(-) create mode 100644 code-queries/allocations-without-ensure-free.ql diff --git a/.github/workflows/codeql-analysis.yaml b/.github/workflows/codeql-analysis.yaml index 6cc31ba01b..5d5bffdc76 100644 --- a/.github/workflows/codeql-analysis.yaml +++ b/.github/workflows/codeql-analysis.yaml @@ -65,7 +65,7 @@ jobs: with: languages: ${{ matrix.language }} build-mode: manual - queries: +./code-queries/term-to-non-term-func.ql,./code-queries/non-term-to-term-func.ql,./code-queries/mismatched-atom-string-length.ql,./code-queries/mismatched-free-type.ql,./code-queries/term-use-after-gc.ql,./code-queries/allocations-exceeding-ensure-free.ql + queries: +./code-queries/term-to-non-term-func.ql,./code-queries/non-term-to-term-func.ql,./code-queries/mismatched-atom-string-length.ql,./code-queries/mismatched-free-type.ql,./code-queries/term-use-after-gc.ql,./code-queries/allocations-exceeding-ensure-free.ql,./code-queries/allocations-without-ensure-free.ql - name: "Build" run: | diff --git a/.github/workflows/esp32-build.yaml b/.github/workflows/esp32-build.yaml index 98726b21f2..12ea218316 100644 --- a/.github/workflows/esp32-build.yaml +++ b/.github/workflows/esp32-build.yaml @@ -74,7 +74,7 @@ jobs: with: languages: "cpp" build-mode: manual - queries: +./code-queries/term-to-non-term-func.ql,./code-queries/non-term-to-term-func.ql,./code-queries/mismatched-atom-string-length.ql,./code-queries/mismatched-free-type.ql,./code-queries/term-use-after-gc.ql,./code-queries/allocations-exceeding-ensure-free.ql + queries: +./code-queries/term-to-non-term-func.ql,./code-queries/non-term-to-term-func.ql,./code-queries/mismatched-atom-string-length.ql,./code-queries/mismatched-free-type.ql,./code-queries/term-use-after-gc.ql,./code-queries/allocations-exceeding-ensure-free.ql,./code-queries/allocations-without-ensure-free.ql - name: Build with idf.py shell: bash diff --git a/.github/workflows/pico-build.yaml b/.github/workflows/pico-build.yaml index 599ea4d479..6ee68a5a1c 100644 --- a/.github/workflows/pico-build.yaml +++ b/.github/workflows/pico-build.yaml @@ -162,7 +162,7 @@ jobs: with: languages: "cpp" build-mode: manual - queries: +./code-queries/term-to-non-term-func.ql,./code-queries/non-term-to-term-func.ql,./code-queries/mismatched-atom-string-length.ql,./code-queries/mismatched-free-type.ql,./code-queries/term-use-after-gc.ql,./code-queries/allocations-exceeding-ensure-free.ql + queries: +./code-queries/term-to-non-term-func.ql,./code-queries/non-term-to-term-func.ql,./code-queries/mismatched-atom-string-length.ql,./code-queries/mismatched-free-type.ql,./code-queries/term-use-after-gc.ql,./code-queries/allocations-exceeding-ensure-free.ql,./code-queries/allocations-without-ensure-free.ql - name: Build shell: bash diff --git a/.github/workflows/stm32-build.yaml b/.github/workflows/stm32-build.yaml index e95d8d23a6..98a20e240d 100644 --- a/.github/workflows/stm32-build.yaml +++ b/.github/workflows/stm32-build.yaml @@ -128,11 +128,12 @@ jobs: run: git config --global --add safe.directory /__w/AtomVM/AtomVM - name: "Initialize CodeQL" + if: matrix.device == 'stm32f407vgt6' uses: github/codeql-action/init@v4 with: languages: 'cpp' build-mode: manual - queries: +./code-queries/term-to-non-term-func.ql,./code-queries/non-term-to-term-func.ql,./code-queries/mismatched-atom-string-length.ql,./code-queries/mismatched-free-type.ql,./code-queries/term-use-after-gc.ql,./code-queries/allocations-exceeding-ensure-free.ql + queries: +./code-queries/term-to-non-term-func.ql,./code-queries/non-term-to-term-func.ql,./code-queries/mismatched-atom-string-length.ql,./code-queries/mismatched-free-type.ql,./code-queries/term-use-after-gc.ql,./code-queries/allocations-exceeding-ensure-free.ql,./code-queries/allocations-without-ensure-free.ql - name: "Build for ${{ matrix.device }}" shell: bash @@ -145,6 +146,7 @@ jobs: cmake --build . - name: "Perform CodeQL Analysis" + if: matrix.device == 'stm32f407vgt6' uses: github/codeql-action/analyze@v4 - name: "Check firmware size for ${{ matrix.device }}" diff --git a/.github/workflows/wasm-build.yaml b/.github/workflows/wasm-build.yaml index 2aafdb0fea..a3be9405cb 100644 --- a/.github/workflows/wasm-build.yaml +++ b/.github/workflows/wasm-build.yaml @@ -62,7 +62,7 @@ jobs: with: languages: ${{matrix.language}} build-mode: manual - queries: +./code-queries/term-to-non-term-func.ql,./code-queries/non-term-to-term-func.ql,./code-queries/mismatched-atom-string-length.ql,./code-queries/mismatched-free-type.ql,./code-queries/term-use-after-gc.ql,./code-queries/allocations-exceeding-ensure-free.ql + queries: +./code-queries/term-to-non-term-func.ql,./code-queries/non-term-to-term-func.ql,./code-queries/mismatched-atom-string-length.ql,./code-queries/mismatched-free-type.ql,./code-queries/term-use-after-gc.ql,./code-queries/allocations-exceeding-ensure-free.ql,./code-queries/allocations-without-ensure-free.ql - name: Compile AtomVM and test modules run: | diff --git a/code-queries/allocations-exceeding-ensure-free.ql b/code-queries/allocations-exceeding-ensure-free.ql index 8574b4728a..0bd22a0153 100644 --- a/code-queries/allocations-exceeding-ensure-free.ql +++ b/code-queries/allocations-exceeding-ensure-free.ql @@ -137,57 +137,95 @@ predicate directParamPlusConstAlloc(Function f, int paramIndex, int constPart) { } /** - * Computes the constant allocation size for a call to a function that - * transitively calls `memory_heap_alloc` without its own ensure_free. - * - * Case 1: The callee has a direct `memory_heap_alloc` with a fully constant size. - * Case 2: The callee has `memory_heap_alloc(heap, constant + param)` and the - * caller passes a constant for that parameter. - * Case 3: Wrapper function -- the callee doesn't directly call `memory_heap_alloc` - * but calls another function that does, and we can compute its size - * via a function-level summary (avoids recursing over calls). + * Holds if `leafCall` is a call reachable from function `f` (through wrapper + * functions that don't call ensure_free) to a function that directly calls + * `memory_heap_alloc`. This is a monotonic reachability predicate that avoids + * non-monotonic recursion through aggregation. + */ +pragma[nomagic] +predicate reachableLeafAllocCall(Function f, FunctionCall leafCall) { + // Base: f directly contains a call to a wrapper that directly allocates + leafCall.getEnclosingFunction() = f and + not isEnsureFreeCall(leafCall) and + not leafCall.getTarget().hasName("memory_heap_alloc") and + directlyCallsHeapAlloc(leafCall.getTarget()) and + not callsEnsureFree(leafCall.getTarget()) + or + // Recursive: f calls wrapper g which has reachable leaf alloc calls + exists(FunctionCall callToG, Function g | + callToG.getEnclosingFunction() = f and + g = callToG.getTarget() and + not callsEnsureFree(g) and + not isEnsureFreeCall(callToG) and + not g.hasName("memory_heap_alloc") and + reachableLeafAllocCall(g, leafCall) + ) +} + +/** + * Gets the constant allocation size for a call to a function that directly + * calls `memory_heap_alloc` (a leaf wrapper). Non-recursive: only considers + * the direct `memory_heap_alloc` calls within the callee. */ pragma[noinline] -int getConstAllocSize(FunctionCall call) { +int leafAllocSize(FunctionCall call) { exists(Function callee | callee = call.getTarget() | - // Case 1: Direct memory_heap_alloc with fully constant size directlyCallsHeapAlloc(callee) and not callsEnsureFree(callee) and - result = directFullyConstAllocSize(callee) and - // Ensure there's no parameter-dependent alloc (avoid double-counting) - not directParamPlusConstAlloc(callee, _, _) - or - // Case 2: Direct memory_heap_alloc with constant + param, caller passes constant - exists(int paramIndex, int constPart, int paramVal | - directlyCallsHeapAlloc(callee) and - not callsEnsureFree(callee) and - directParamPlusConstAlloc(callee, paramIndex, constPart) and - paramVal = constExprValue(call.getArgument(paramIndex)) and - result = constPart + paramVal - ) - or - // Case 3: Wrapper function -- use function-level summary - not directlyCallsHeapAlloc(callee) and - not callsEnsureFree(callee) and - transitivelyCallsHeapAllocWithoutEnsureFree(callee) and - result = funcConstAllocSize(callee) + result = + sum(int s | s = directFullyConstAllocSize(callee) | s) + + + sum(int paramIndex, int constPart | + directParamPlusConstAlloc(callee, paramIndex, constPart) and + exists(constExprValue(call.getArgument(paramIndex))) + | constPart + constExprValue(call.getArgument(paramIndex))) ) } /** - * Gets the constant allocation size reachable within function `f` - * (through inner calls that allocate without their own ensure_free). - * This is a function-level summary that avoids per-call recursion. - * May return multiple values; callers use maxConstAllocSize to take the max. + * Computes the total constant allocation size for a call to a function that + * transitively calls `memory_heap_alloc` without its own ensure_free. + * + * Aggregates all applicable constant contributions from the callee: + * - All fully-constant direct `memory_heap_alloc` sizes + * - All `memory_heap_alloc(heap, constant + param)` where the caller passes + * a constant for that parameter + * - All transitive wrapper function contributions (via reachableLeafAllocCall) */ -pragma[nomagic] -int funcConstAllocSize(Function f) { - exists(FunctionCall innerCall | - innerCall.getEnclosingFunction() = f and - transitivelyCallsHeapAllocWithoutEnsureFree(innerCall.getTarget()) and - not isEnsureFreeCall(innerCall) and - not innerCall.getTarget().hasName("memory_heap_alloc") and - result = getConstAllocSize(innerCall) +pragma[noinline] +int getConstAllocSize(FunctionCall call) { + exists(Function callee | callee = call.getTarget() | + not callsEnsureFree(callee) and + transitivelyCallsHeapAllocWithoutEnsureFree(callee) and + // At least one constant contribution must exist + ( + exists(directFullyConstAllocSize(callee)) + or + exists(int pi, int cp | + directParamPlusConstAlloc(callee, pi, cp) and + exists(constExprValue(call.getArgument(pi))) + ) + or + exists(FunctionCall leafCall | + reachableLeafAllocCall(callee, leafCall) and + exists(leafAllocSize(leafCall)) + ) + ) and + result = + // Sum all fully-constant direct allocations + sum(int s | s = directFullyConstAllocSize(callee) | s) + + + // Sum all param+const allocations where caller passes a constant + sum(int paramIndex, int constPart | + directParamPlusConstAlloc(callee, paramIndex, constPart) and + exists(constExprValue(call.getArgument(paramIndex))) + | constPart + constExprValue(call.getArgument(paramIndex))) + + + // Sum transitive wrapper contributions via reachable leaf calls + sum(FunctionCall leafCall, int leafSize | + reachableLeafAllocCall(callee, leafCall) and + leafSize = leafAllocSize(leafCall) + | leafSize) ) } diff --git a/code-queries/allocations-without-ensure-free.ql b/code-queries/allocations-without-ensure-free.ql new file mode 100644 index 0000000000..183cc3c254 --- /dev/null +++ b/code-queries/allocations-without-ensure-free.ql @@ -0,0 +1,248 @@ +/** + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * @name Allocations without ensure_free in NIFs and port handlers + * @kind problem + * @problem.severity error + * @id atomvm/allocations-without-ensure-free + * @tags correctness + * @precision high + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +import cpp +import semmle.code.cpp.controlflow.BasicBlocks + +/** + * Holds if the function `f` directly calls `memory_heap_alloc`. + */ +predicate directlyCallsHeapAlloc(Function f) { + exists(FunctionCall fc | + fc.getEnclosingFunction() = f and + fc.getTarget().hasName("memory_heap_alloc") + ) +} + +/** + * Holds if `f` directly calls any memory_ensure_free variant. + */ +pragma[noinline] +predicate directlyCallsEnsureFree(Function f) { + exists(FunctionCall fc | + fc.getEnclosingFunction() = f and + ( + fc.getTarget().hasName("memory_ensure_free") or + fc.getTarget().hasName("memory_ensure_free_opt") or + fc.getTarget().hasName("memory_ensure_free_with_roots") or + fc.getTarget().hasName("memory_erl_nif_env_ensure_free") or + fc.getTarget().hasName("memory_init_heap") or + fc.getTarget().hasName("memory_init_heap_root_fragment") + ) + ) +} + +/** + * Holds if `caller` directly calls `callee`. + */ +pragma[noinline] +predicate callEdge(Function caller, Function callee) { + exists(FunctionCall fc | + fc.getEnclosingFunction() = caller and + callee = fc.getTarget() + ) +} + +/** + * Holds if `f` directly or transitively calls any memory_ensure_free variant. + */ +pragma[nomagic] +predicate callsEnsureFree(Function f) { + directlyCallsEnsureFree(f) + or + exists(Function callee | + callEdge(f, callee) and + callsEnsureFree(callee) + ) +} + +/** + * Holds if `f` transitively calls `memory_heap_alloc` (directly or through callees) + * AND does not call any ensure_free variant (meaning it relies on the caller to + * have ensured enough heap space). + */ +pragma[nomagic] +predicate transitivelyCallsHeapAllocWithoutEnsureFree(Function f) { + directlyCallsHeapAlloc(f) and not callsEnsureFree(f) + or + not callsEnsureFree(f) and + exists(Function callee | + callEdge(f, callee) and + transitivelyCallsHeapAllocWithoutEnsureFree(callee) + ) +} + +/** + * Holds if `fc` is a call to one of the memory_ensure_free variants. + */ +predicate isEnsureFreeCall(FunctionCall fc) { + fc.getTarget().hasName("memory_ensure_free") + or + fc.getTarget().hasName("memory_ensure_free_opt") + or + fc.getTarget().hasName("memory_ensure_free_with_roots") + or + fc.getTarget().hasName("memory_erl_nif_env_ensure_free") + or + fc.getTarget().hasName("memory_init_heap") + or + fc.getTarget().hasName("memory_init_heap_root_fragment") +} + +/** + * Holds if `allocCall` is a function call that transitively calls + * `memory_heap_alloc` without its own ensure_free. + */ +predicate isAllocatingCall(FunctionCall allocCall) { + transitivelyCallsHeapAllocWithoutEnsureFree(allocCall.getTarget()) and + not isEnsureFreeCall(allocCall) and + not allocCall.getTarget().hasName("memory_heap_alloc") +} + +/** + * Holds if `f` is a NIF: returns `term` and has parameters (Context *, int, term []). + */ +predicate isNif(Function f) { + f.getNumberOfParameters() = 3 and + f.getType().toString() = "term" and + f.getParameter(0).getType().stripType().(Struct).hasName("Context") and + f.getParameter(2).getType().toString() = "term[]" +} + +/** + * Holds if `f` is a port handler: returns `NativeHandlerResult` and has + * a single parameter (Context *). + */ +predicate isPortHandler(Function f) { + f.getNumberOfParameters() = 1 and + f.getType().toString() = "NativeHandlerResult" and + f.getParameter(0).getType().stripType().(Struct).hasName("Context") +} + +/** + * Holds if `f` is a NIF or port handler entry point. + */ +predicate isEntryPoint(Function f) { + isNif(f) or isPortHandler(f) +} + +/** + * Holds if `bb` contains at least one call to an ensure_free variant + * or to a function that transitively calls ensure_free. + */ +predicate isBarrierBlock(BasicBlock bb) { + exists(FunctionCall fc | + fc.getBasicBlock() = bb and + ( + isEnsureFreeCall(fc) + or + callsEnsureFree(fc.getTarget()) + ) + ) +} + +/** + * Holds if `bb` is reachable from the entry point of `f` via a path + * that does not pass through any barrier block (a block containing + * an ensure_free call or a call that transitively calls ensure_free). + */ +pragma[nomagic] +predicate reachableFromEntryAvoidingBarriers(BasicBlock bb, Function f) { + bb = f.getEntryPoint().getBasicBlock() and + not isBarrierBlock(bb) + or + exists(BasicBlock pred | + reachableFromEntryAvoidingBarriers(pred, f) and + pred.getASuccessor() = bb and + not isBarrierBlock(bb) + ) +} + +/** + * Holds if there is an ensure_free call (or a call to a function that + * transitively calls ensure_free) before `allocCall` within the same + * basic block. + */ +predicate hasEnsureFreeBeforeInBlock(FunctionCall allocCall) { + exists(FunctionCall ef, BasicBlock bb, int efIdx, int allocIdx | + (isEnsureFreeCall(ef) or callsEnsureFree(ef.getTarget())) and + bb = allocCall.getBasicBlock() and + ef.getBasicBlock() = bb and + bb.getNode(efIdx) = ef and + bb.getNode(allocIdx) = allocCall and + efIdx < allocIdx + ) +} + +/** + * Holds if the alloc call has a NOLINT(allocations-without-ensure-free) + * suppression comment on the same line or the line before. + */ +predicate hasSuppressionComment(FunctionCall allocCall) { + exists(Comment c | + c.getLocation().getFile() = allocCall.getLocation().getFile() and + ( + c.getLocation().getStartLine() = allocCall.getLocation().getStartLine() or + c.getLocation().getStartLine() = allocCall.getLocation().getStartLine() - 1 + ) and + c.getContents().matches("%NOLINT(allocations-without-ensure-free)%") + ) +} + +/** + * Holds if `allocCall` is an allocating call (or direct memory_heap_alloc call) + * inside an entry point that can be reached from the function entry without + * passing through any ensure_free call on every path. + */ +predicate allocWithoutPrecedingEnsureFree(FunctionCall allocCall) { + isEntryPoint(allocCall.getEnclosingFunction()) and + (isAllocatingCall(allocCall) or allocCall.getTarget().hasName("memory_heap_alloc")) and + not hasEnsureFreeBeforeInBlock(allocCall) and + exists(Function f, BasicBlock allocBB | + f = allocCall.getEnclosingFunction() and + allocBB = allocCall.getBasicBlock() and + ( + // allocBB is reachable from entry through non-barrier blocks + reachableFromEntryAvoidingBarriers(allocBB, f) + or + // allocBB is itself a barrier (ensure_free is in the block but after the alloc) + // and is reachable from entry through non-barrier blocks + isBarrierBlock(allocBB) and + ( + allocBB = f.getEntryPoint().getBasicBlock() + or + exists(BasicBlock pred | + reachableFromEntryAvoidingBarriers(pred, f) and + pred.getASuccessor() = allocBB + ) + ) + ) + ) +} + +from FunctionCall allocCall, Function entryPoint, string entryKind +where + entryPoint = allocCall.getEnclosingFunction() and + ( + isNif(entryPoint) and entryKind = "NIF" + or + isPortHandler(entryPoint) and entryKind = "port handler" + ) and + allocWithoutPrecedingEnsureFree(allocCall) and + not hasSuppressionComment(allocCall) +select allocCall, + "Call to " + allocCall.getTarget().getName() + + " allocates without any preceding ensure_free in " + entryKind + " $@.", + entryPoint, entryPoint.getName() diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 9466363a9a..aac5dd4981 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -3018,6 +3018,7 @@ static term nif_erlang_process_info(Context *ctx, int argc, term argv[]) term ret = term_invalid_term(); if (ctx == target) { size_t term_size; + // NOLINT(allocations-without-ensure-free) called with NULL heap, only computes size if (UNLIKELY(!context_get_process_info(ctx, NULL, &term_size, item, NULL))) { globalcontext_get_process_unlock(ctx->global, target); RAISE_ERROR(BADARG_ATOM); @@ -3081,7 +3082,11 @@ static term nif_erlang_system_info(Context *ctx, int argc, term argv[]) return term_from_literal_binary((const uint8_t *) buf, len, &ctx->heap, ctx->global); } if (key == ATOMVM_VERSION_ATOM) { - return term_from_literal_binary((const uint8_t *) ATOMVM_VERSION, strlen(ATOMVM_VERSION), &ctx->heap, ctx->global); + size_t len = strlen(ATOMVM_VERSION); + if (memory_ensure_free_opt(ctx, term_binary_heap_size(len), MEMORY_CAN_SHRINK) != MEMORY_GC_OK) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return term_from_literal_binary((const uint8_t *) ATOMVM_VERSION, len, &ctx->heap, ctx->global); } if (key == SYSTEM_VERSION_ATOM) { char system_version[256]; @@ -6502,6 +6507,7 @@ static term nif_lists_flatten(Context *ctx, int argc, term argv[]) break; } + // NOLINT(allocations-without-ensure-free) ensure_free was called when result_len > tail_len, and this path is only reached in that case term *new_list_item = term_list_alloc(&ctx->heap); if (prev_term) { prev_term[0] = term_list_from_list_ptr(new_list_item); diff --git a/src/platforms/esp32/components/avm_builtins/dac_driver.c b/src/platforms/esp32/components/avm_builtins/dac_driver.c index 9f55694e3c..878cd15454 100644 --- a/src/platforms/esp32/components/avm_builtins/dac_driver.c +++ b/src/platforms/esp32/components/avm_builtins/dac_driver.c @@ -129,6 +129,8 @@ static term nif_oneshot_new_channel_p(Context *ctx, int argc, term argv[]) if (!err) { if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(3) + REF_SIZE + TUPLE_SIZE(2), 1, &chan_obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { ESP_LOGE(TAG, "failed to allocate memory for result: %s:%i.", __FILE__, __LINE__); + dac_oneshot_del_channel(chan_rsrc->handle); + chan_rsrc->handle = NULL; RAISE_ERROR(OUT_OF_MEMORY_ATOM); } diff --git a/src/platforms/esp32/components/avm_sys/platform_nifs.c b/src/platforms/esp32/components/avm_sys/platform_nifs.c index 2b554333b0..3db18b6c53 100644 --- a/src/platforms/esp32/components/avm_sys/platform_nifs.c +++ b/src/platforms/esp32/components/avm_sys/platform_nifs.c @@ -710,6 +710,9 @@ static term nif_esp_get_default_mac(Context *ctx, int argc, term argv[]) esp_err_t err = esp_efuse_mac_get_default(mac); if (err != ESP_OK) { ESP_LOGE(TAG, "Unable to read default mac. err=%i", err); + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } return port_create_error_tuple(ctx, esp_err_to_term(ctx->global, err)); } @@ -832,6 +835,9 @@ static term nif_esp_task_wdt_deinit(Context *ctx, int argc, term argv[]) return OK_ATOM; } + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } term error_tuple = term_alloc_tuple(2, &ctx->heap); term_put_tuple_element(error_tuple, 0, ERROR_ATOM); term_put_tuple_element(error_tuple, 1, term_from_int(result)); @@ -927,7 +933,12 @@ static term nif_esp_timer_get_time(Context *ctx, int argc, term argv[]) UNUSED(argv); UNUSED(argc); - return term_make_maybe_boxed_int64(esp_timer_get_time(), &ctx->heap); + uint64_t val = esp_timer_get_time(); + size_t term_size = term_boxed_integer_size(val); + if (UNLIKELY(memory_ensure_free_opt(ctx, term_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return term_make_maybe_boxed_int64(val, &ctx->heap); } //