From aea7bf1865a517a1f18046c839bffe66cdf267dc Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sat, 14 Feb 2026 20:21:17 +0100 Subject: [PATCH] Add CodeQL query to check allocations do not exceed ensure_free The query also checks redundant ensure_free calls, i.e. calls followed by another call with no allocation in between. Fix errors found by the query: - Add a missing ensure_free in esp32 `dac_driver.c` - Fix and simplify allocation in `adc_driver.c` (fix misplaced parenthesis bug and merge two ensure_free calls into one) - Remove redundant ensure_free calls in `otp_ssl.c` (left over from removal of `enif_make_resource` which used to do its own ensure_free) - Remove a redundant ensure_free call in `nif_erlang_fun_to_list` 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 | 13 + .github/workflows/wasm-build.yaml | 2 +- .../allocations-exceeding-ensure-free.ql | 522 ++++++++++++++++++ src/libAtomVM/nifs.c | 5 - src/libAtomVM/otp_ssl.c | 16 - .../components/avm_builtins/adc_driver.c | 32 +- .../components/avm_builtins/dac_driver.c | 6 + 10 files changed, 557 insertions(+), 45 deletions(-) create mode 100644 code-queries/allocations-exceeding-ensure-free.ql diff --git a/.github/workflows/codeql-analysis.yaml b/.github/workflows/codeql-analysis.yaml index b6b744d623..6cc31ba01b 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 + 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 - name: "Build" run: | diff --git a/.github/workflows/esp32-build.yaml b/.github/workflows/esp32-build.yaml index 4725f987a9..98726b21f2 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 + 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 - name: Build with idf.py shell: bash diff --git a/.github/workflows/pico-build.yaml b/.github/workflows/pico-build.yaml index 379add078f..028556716b 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 + 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 - name: Build shell: bash diff --git a/.github/workflows/stm32-build.yaml b/.github/workflows/stm32-build.yaml index 440c1743e1..e95d8d23a6 100644 --- a/.github/workflows/stm32-build.yaml +++ b/.github/workflows/stm32-build.yaml @@ -124,6 +124,16 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 + - name: "Git config safe.directory for codeql" + run: git config --global --add safe.directory /__w/AtomVM/AtomVM + + - name: "Initialize CodeQL" + 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 + - name: "Build for ${{ matrix.device }}" shell: bash working-directory: ./src/platforms/stm32/ @@ -134,6 +144,9 @@ jobs: cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/arm-toolchain.cmake -DDEVICE=${{ matrix.device }} cmake --build . + - name: "Perform CodeQL Analysis" + uses: github/codeql-action/analyze@v4 + - name: "Check firmware size for ${{ matrix.device }}" shell: bash working-directory: ./src/platforms/stm32/build diff --git a/.github/workflows/wasm-build.yaml b/.github/workflows/wasm-build.yaml index 321b669f26..5cfe3bd60e 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 + 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 - 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 new file mode 100644 index 0000000000..8574b4728a --- /dev/null +++ b/code-queries/allocations-exceeding-ensure-free.ql @@ -0,0 +1,522 @@ +/** + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * @name Allocations exceeding ensure_free + * @kind problem + * @problem.severity error + * @id atomvm/allocations-exceeding-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.Dominance + +/** + * Gets the constant integer value of expression `e`, either directly + * from a compile-time constant expression, or by tracing through a + * local variable that was initialized with a constant expression and + * never reassigned (no assignments or increment/decrement operations). + */ +pragma[noinline] +int constExprValue(Expr e) { + result = e.getValue().toInt() + or + exists(LocalVariable v | + e.(VariableAccess).getTarget() = v and + result = v.getInitializer().getExpr().getValue().toInt() and + // Only trace if the variable is never modified after initialization + not exists(Assignment a | a.getLValue().(VariableAccess).getTarget() = v) and + not exists(CrementOperation co | co.getOperand().(VariableAccess).getTarget() = v) + ) +} + +/** + * 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") + ) + ) +} + +/** + * Holds if `caller` directly calls `callee` (cached edge relation for + * call-graph traversal, avoiding repeated FunctionCall joins). + */ +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. + * Functions that do this manage their own heap allocation and should not be + * checked against the caller's ensure_free. + */ +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) + ) +} + +/** + * Gets the constant allocation size for a direct `memory_heap_alloc(heap, size)` call + * within function `f`, when the size argument is fully constant. + */ +int directFullyConstAllocSize(Function f) { + exists(FunctionCall fc | + fc.getEnclosingFunction() = f and + fc.getTarget().hasName("memory_heap_alloc") and + result = constExprValue(fc.getArgument(1)) + ) +} + +/** + * Gets the index of the parameter that flows into the `memory_heap_alloc` size + * argument as part of an addition with a constant, within function `f`. + * Also binds `constPart` to the constant part of that addition. + */ +predicate directParamPlusConstAlloc(Function f, int paramIndex, int constPart) { + exists(FunctionCall fc, AddExpr add, Expr constOperand, Expr paramOperand | + fc.getEnclosingFunction() = f and + fc.getTarget().hasName("memory_heap_alloc") and + add = fc.getArgument(1) and + ( + constOperand = add.getLeftOperand() and paramOperand = add.getRightOperand() + or + constOperand = add.getRightOperand() and paramOperand = add.getLeftOperand() + ) and + constPart = constExprValue(constOperand) and + paramOperand.(VariableAccess).getTarget() = f.getParameter(paramIndex) + ) +} + +/** + * 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). + */ +pragma[noinline] +int getConstAllocSize(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) + ) +} + +/** + * 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. + */ +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) + ) +} + +/** + * Holds if the function call `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") +} + +/** + * Holds if `fc` is an ensure_free call that actually reserves heap space + * (i.e., size > 0). Calls with size 0 are GC/shrink operations and don't + * establish an allocation budget. + */ +predicate isReservingEnsureFreeCall(FunctionCall fc) { + isEnsureFreeCall(fc) and + not constExprValue(fc.getArgument(1)) = 0 +} + +/** + * Holds if `allocCall` is a function call that transitively calls + * `memory_heap_alloc` without its own ensure_free (i.e., it relies on + * the caller to have ensured enough heap space). + */ +predicate isAllocatingCall(FunctionCall allocCall) { + transitivelyCallsHeapAllocWithoutEnsureFree(allocCall.getTarget()) and + // Exclude the ensure_free functions themselves + not isEnsureFreeCall(allocCall) and + // Exclude memory_heap_alloc itself (we care about wrapper calls) + not allocCall.getTarget().hasName("memory_heap_alloc") +} + +/** + * Gets the position of a control-flow node within its basic block. + * Used for precise intra-BB ordering instead of line numbers. + */ +pragma[noinline] +int nodeIndexInBB(ControlFlowNode node, BasicBlock bb) { + bb.getNode(result) = node +} + +/** + * Holds if `before` precedes `after` in the CFG. For the same basic block, + * uses node position. For different basic blocks, uses dominance (which + * guarantees execution order). + */ +pragma[inline] +predicate cfgPrecedes(ControlFlowNode before, BasicBlock beforeBB, ControlFlowNode after, BasicBlock afterBB) { + beforeBB = afterBB and + nodeIndexInBB(before, beforeBB) < nodeIndexInBB(after, afterBB) + or + beforeBB != afterBB and + bbStrictlyDominates(beforeBB, afterBB) +} + +/** + * Gets the nearest preceding reserving ensure_free call that dominates + * the allocation in the CFG. Uses dominance to correctly scope across + * switch cases (an ensure_free in one case does not dominate another case). + * Uses CFG node ordering instead of line numbers for precise ordering. + */ +pragma[nomagic] +FunctionCall nearestPrecedingEnsureFree(FunctionCall allocCall) { + exists(Function enclosing, BasicBlock allocBB, BasicBlock resultBB | + enclosing = allocCall.getEnclosingFunction() and + allocBB = allocCall.getBasicBlock() and + result.getEnclosingFunction() = enclosing and + isReservingEnsureFreeCall(result) and + resultBB = result.getBasicBlock() and + // Ensure_free must precede alloc in CFG + cfgPrecedes(result, resultBB, allocCall, allocBB) and + // No other dominating reserving ensure_free between them + not exists(FunctionCall other, BasicBlock otherBB | + isReservingEnsureFreeCall(other) and + other.getEnclosingFunction() = enclosing and + otherBB = other.getBasicBlock() and + cfgPrecedes(result, resultBB, other, otherBB) and + cfgPrecedes(other, otherBB, allocCall, allocBB) + ) + ) +} + +/** + * Gets the worst-case (maximum) constant allocation size for a single call. + * When getConstAllocSize returns multiple values (e.g., from multiple + * allocation paths within the callee), takes the maximum. + */ +pragma[noinline] +int maxConstAllocSize(FunctionCall call) { + result = max(int size | size = getConstAllocSize(call)) +} + +/** + * Cached mapping from allocating call to its nearest preceding ensure_free. + * Materializing this relation avoids repeated evaluation of + * nearestPrecedingEnsureFree inside the sum aggregation. + */ +pragma[nomagic] +predicate allocToBudget(FunctionCall allocCall, FunctionCall ensureFreeCall) { + isAllocatingCall(allocCall) and + ensureFreeCall = nearestPrecedingEnsureFree(allocCall) +} + +/** + * Computes the cumulative allocation size at a given allocation call, + * summing the worst-case allocation of all preceding calls that share + * the same ensure_free budget, using CFG ordering (dominance for cross-BB, + * node position for same-BB). + */ +pragma[noinline] +int cumulativeAllocSize(FunctionCall allocCall, FunctionCall ensureFreeCall) { + exists(BasicBlock allocBB | + allocBB = allocCall.getBasicBlock() and + result = + sum(FunctionCall other, int otherSize | + other.getEnclosingFunction() = allocCall.getEnclosingFunction() and + allocToBudget(other, ensureFreeCall) and + otherSize = maxConstAllocSize(other) and + ( + other = allocCall + or + exists(BasicBlock otherBB | + otherBB = other.getBasicBlock() and + cfgPrecedes(other, otherBB, allocCall, allocBB) + ) + ) + | + otherSize + ) + ) +} + +// ============================================================ +// Symbolic analysis: ensure_free(ctx, var + C) with allocations +// that pass `var` as a parameter to functions like term_alloc_tuple. +// ============================================================ + +/** + * Holds if expression `e` decomposes into `v + c` where `v` is a variable + * and `c` is a compile-time constant. Handles: + * - Direct AddExpr: `v + c` or `c + v` (including macro-expanded forms) + * - Variable tracing: a non-reassigned local variable whose initializer + * is itself `v + c` + */ +pragma[noinline] +predicate exprIsVarPlusConst(Expr e, Variable v, int c) { + exists(AddExpr add | + add = e and + ( + add.getLeftOperand().(VariableAccess).getTarget() = v and + c = add.getRightOperand().getValue().toInt() + or + add.getRightOperand().(VariableAccess).getTarget() = v and + c = add.getLeftOperand().getValue().toInt() + ) + ) + or + // Trace through a non-reassigned local variable to its initializer + exists(LocalVariable lv | + e.(VariableAccess).getTarget() = lv and + not exists(Assignment a | a.getLValue().(VariableAccess).getTarget() = lv) and + not exists(CrementOperation co | co.getOperand().(VariableAccess).getTarget() = lv) and + exprIsVarPlusConst(lv.getInitializer().getExpr(), v, c) + ) +} + +/** + * Gets the constant part of a symbolic ensure_free size expression. + * Holds when the ensure_free size argument is `sharedVar + constPart`. + * Only matches when the size is NOT a fully-constant expression + * (those are handled by the constant analysis path). + */ +pragma[noinline] +predicate ensureFreeSymbolicConstPart( + FunctionCall efCall, Variable sharedVar, int constPart +) { + isReservingEnsureFreeCall(efCall) and + exprIsVarPlusConst(efCall.getArgument(1), sharedVar, constPart) and + // Only for truly symbolic cases (not already handled by constant path) + not exists(constExprValue(efCall.getArgument(1))) +} + +/** + * Gets the constant part of an allocation call's size when the caller + * passes `sharedVar` directly as the parameter to a function with + * `memory_heap_alloc(heap, constPart + param)`. + */ +pragma[noinline] +int symbolicAllocConstPart(FunctionCall allocCall, Variable sharedVar) { + exists(Function callee, int paramIndex | + callee = allocCall.getTarget() and + not callsEnsureFree(callee) and + directParamPlusConstAlloc(callee, paramIndex, result) and + allocCall.getArgument(paramIndex).(VariableAccess).getTarget() = sharedVar + ) +} + +/** + * Gets the effective constant cost of an allocation relative to a shared + * variable. Only counts allocations that provably share the variable, + * returning just the constant offset (the variable part cancels with the + * ensure_free). Allocations that don't share the variable are skipped + * to avoid false positives from functions that consume the variable + * budget through a different parameter interface. + */ +pragma[noinline] +int effectiveConstCost(FunctionCall allocCall, Variable sharedVar) { + result = symbolicAllocConstPart(allocCall, sharedVar) +} + +/** + * Computes the cumulative effective constant cost at a given allocation call + * under a symbolic ensure_free. Sums the effective constant costs of all + * preceding allocations that share the same ensure_free and variable, + * using CFG ordering (dominance for cross-BB, node position for same-BB). + */ +pragma[noinline] +int cumulativeEffectiveConstCost( + FunctionCall allocCall, FunctionCall ensureFreeCall, Variable sharedVar +) { + exists(BasicBlock allocBB | + allocBB = allocCall.getBasicBlock() and + result = + sum(FunctionCall other, int otherCost | + other.getEnclosingFunction() = allocCall.getEnclosingFunction() and + allocToBudget(other, ensureFreeCall) and + otherCost = effectiveConstCost(other, sharedVar) and + ( + other = allocCall + or + exists(BasicBlock otherBB | + otherBB = other.getBasicBlock() and + cfgPrecedes(other, otherBB, allocCall, allocBB) + ) + ) + | + otherCost + ) + ) +} + +// ============================================================ +// Redundant ensure_free detection: a reserving ensure_free +// whose budget is never used because a subsequent call resets +// the heap budget (by calling ensure_free internally). +// ============================================================ + +/** + * Holds if `efCall` is a redundant reserving ensure_free: no allocating call + * uses its budget, and `supersedingCall` is a subsequent call that resets + * the heap budget (either a direct ensure_free or a function like + * enif_make_resource that internally calls ensure_free). + */ +pragma[nomagic] +predicate isRedundantEnsureFree(FunctionCall efCall, FunctionCall supersedingCall) { + isReservingEnsureFreeCall(efCall) and + // No allocating call uses this ensure_free's budget + not exists(FunctionCall a | allocToBudget(a, efCall)) and + // Find a subsequent call that resets the heap budget + exists(BasicBlock efBB, BasicBlock superBB | + efBB = efCall.getBasicBlock() and + supersedingCall.getEnclosingFunction() = efCall.getEnclosingFunction() and + superBB = supersedingCall.getBasicBlock() and + ( + // Another direct reserving ensure_free call + isReservingEnsureFreeCall(supersedingCall) and + supersedingCall != efCall + or + // A function that internally calls ensure_free (e.g., enif_make_resource) + not isEnsureFreeCall(supersedingCall) and + callsEnsureFree(supersedingCall.getTarget()) + ) and + cfgPrecedes(efCall, efBB, supersedingCall, superBB) and + // Exclude superseding calls on the error-handling path of the + // ensure_free's own failure check. Pattern: + // if (UNLIKELY(ensure_free(...) != MEMORY_GC_OK)) { + // RAISE_ERROR(...); // contains stacktrace_create_raw + // } + // When the if-condition is true (ensure_free FAILED), we enter + // the error handler. Superseding calls there are irrelevant + // because the ensure_free budget was never established. + not exists(BasicBlock trueBB | + trueBB = efBB.getATrueSuccessor() and + bbDominates(trueBB, superBB) + ) + ) +} + +from FunctionCall problemCall, string msg, FunctionCall relatedCall, string relatedLabel +where + ( + // Allocation exceeding budget + exists(int cumCost, int budget | + allocToBudget(problemCall, relatedCall) and + ( + // Fully constant comparison + exists(maxConstAllocSize(problemCall)) and + cumCost = cumulativeAllocSize(problemCall, relatedCall) and + budget = constExprValue(relatedCall.getArgument(1)) and + cumCost > budget + or + // Symbolic comparison (ensure_free is var + const, + // allocations share the same variable) + exists(Variable sharedVar | + ensureFreeSymbolicConstPart(relatedCall, sharedVar, budget) and + exists(effectiveConstCost(problemCall, sharedVar)) and + cumCost = cumulativeEffectiveConstCost(problemCall, relatedCall, sharedVar) + ) and + cumCost > budget + ) and + msg = + "Cumulative constant-part allocation of " + cumCost.toString() + + " terms exceeds ensure_free budget of " + budget.toString() + " terms at $@." + ) and + relatedLabel = "this ensure_free call" + ) + or + ( + // Redundant ensure_free + isRedundantEnsureFree(problemCall, relatedCall) and + msg = + "Redundant ensure_free: no allocations occur before $@ which resets the heap budget." and + relatedLabel = relatedCall.getTarget().getName() + ) +select problemCall, msg, relatedCall, relatedLabel diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 42abf74289..9466363a9a 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -4040,11 +4040,6 @@ static term nif_erlang_fun_to_list(Context *ctx, int argc, term argv[]) RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - if (UNLIKELY(memory_ensure_free_opt(ctx, str_len * 2, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - free(buf); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - // it looks like unicode is not supported right now for module names but it looks like a // compiler limitation rather than a BEAM limitation, so let's assume that one day they might // be unicode. diff --git a/src/libAtomVM/otp_ssl.c b/src/libAtomVM/otp_ssl.c index d7ded0b6ff..a9658a6fc0 100644 --- a/src/libAtomVM/otp_ssl.c +++ b/src/libAtomVM/otp_ssl.c @@ -226,10 +226,6 @@ static term nif_ssl_entropy_init(Context *ctx, int argc, term argv[]) UNUSED(argc); UNUSED(argv); - if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_REFERENCE_RESOURCE_SIZE) != MEMORY_GC_OK)) { - AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } struct EntropyContextResource *rsrc_obj = enif_alloc_resource(entropycontext_resource_type, sizeof(struct EntropyContextResource)); if (IS_NULL_PTR(rsrc_obj)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); @@ -253,10 +249,6 @@ static term nif_ssl_ctr_drbg_init(Context *ctx, int argc, term argv[]) UNUSED(argc); UNUSED(argv); - if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_REFERENCE_RESOURCE_SIZE) != MEMORY_GC_OK)) { - AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } struct CtrDrbgResource *rsrc_obj = enif_alloc_resource(ctrdrbg_resource_type, sizeof(struct CtrDrbgResource)); if (IS_NULL_PTR(rsrc_obj)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); @@ -309,10 +301,6 @@ static term nif_ssl_init(Context *ctx, int argc, term argv[]) UNUSED(argc); UNUSED(argv); - if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_REFERENCE_RESOURCE_SIZE) != MEMORY_GC_OK)) { - AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } struct SSLContextResource *rsrc_obj = enif_alloc_resource(sslcontext_resource_type, sizeof(struct SSLContextResource)); if (IS_NULL_PTR(rsrc_obj)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); @@ -366,10 +354,6 @@ static term nif_ssl_config_init(Context *ctx, int argc, term argv[]) UNUSED(argc); UNUSED(argv); - if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_REFERENCE_RESOURCE_SIZE) != MEMORY_GC_OK)) { - AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } struct SSLConfigResource *rsrc_obj = enif_alloc_resource(sslconfig_resource_type, sizeof(struct SSLConfigResource)); if (IS_NULL_PTR(rsrc_obj)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); diff --git a/src/platforms/esp32/components/avm_builtins/adc_driver.c b/src/platforms/esp32/components/avm_builtins/adc_driver.c index ecc3d52c75..bbc6c2199b 100644 --- a/src/platforms/esp32/components/avm_builtins/adc_driver.c +++ b/src/platforms/esp32/components/avm_builtins/adc_driver.c @@ -343,21 +343,18 @@ static term nif_adc_init(Context *ctx, int argc, term argv[]) } #endif - if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { - enif_release_resource(unit_rsrc); - ESP_LOGE(TAG, "failed to allocate memory for resource: %s:%i.", __FILE__, __LINE__); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - ERL_NIF_TERM unit_obj = term_from_resource(unit_rsrc, &ctx->heap); - enif_release_resource(unit_rsrc); // decrement refcount after enif_alloc_resource - // {ok, {'$adc', Unit :: resource(), ref()}} - size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(3) + REF_SIZE; + size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(3) + REF_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE; ESP_LOGD(TAG, "Requesting memory size %u for return message", requested_size); - if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &unit_obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { + enif_release_resource(unit_rsrc); ESP_LOGE(TAG, "failed to allocate tuple memory size %u: %s:%i.", requested_size, __FILE__, __LINE__); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } + + term unit_obj = term_from_resource(unit_rsrc, &ctx->heap); + enif_release_resource(unit_rsrc); // decrement refcount after enif_alloc_resource + term unit_resource = term_alloc_tuple(3, &ctx->heap); term_put_tuple_element(unit_resource, 0, globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "$adc"))); term_put_tuple_element(unit_resource, 1, unit_obj); @@ -494,23 +491,18 @@ static term nif_adc_acquire(Context *ctx, int argc, term argv[]) chan_rsrc->channel = adc_channel; chan_rsrc->calibration = calibration; - if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE != MEMORY_GC_OK))) { + // {ok, {'$adc', resource(), ref()}} + size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(3) + REF_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE; + ESP_LOGD(TAG, "Requesting memory size %u for return message", requested_size); + if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { enif_release_resource(chan_rsrc); - ESP_LOGE(TAG, "failed to allocate memory for resource: %s:%i.", __FILE__, __LINE__); + ESP_LOGE(TAG, "failed to allocate tuple memory size %u: %s:%i.", requested_size, __FILE__, __LINE__); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } term chan_obj = term_from_resource(chan_rsrc, &ctx->heap); enif_release_resource(chan_rsrc); // decrement refcount after enif_alloc_resource - // {ok, {'$adc', resource(), ref()}} - size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(3) + REF_SIZE; - ESP_LOGD(TAG, "Requesting memory size %u for return message", requested_size); - if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &chan_obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - ESP_LOGE(TAG, "failed to allocate tuple memory size %u: %s:%i.", requested_size, __FILE__, __LINE__); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - term chan_resource = term_alloc_tuple(3, &ctx->heap); term_put_tuple_element(chan_resource, 0, globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "$adc"))); term_put_tuple_element(chan_resource, 1, chan_obj); diff --git a/src/platforms/esp32/components/avm_builtins/dac_driver.c b/src/platforms/esp32/components/avm_builtins/dac_driver.c index b5750f0c44..9f55694e3c 100644 --- a/src/platforms/esp32/components/avm_builtins/dac_driver.c +++ b/src/platforms/esp32/components/avm_builtins/dac_driver.c @@ -22,6 +22,7 @@ // References // https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/dac.html +#include "memory.h" #include #ifdef CONFIG_AVM_ENABLE_DAC_NIF #include @@ -126,6 +127,11 @@ static term nif_oneshot_new_channel_p(Context *ctx, int argc, term argv[]) enif_release_resource(chan_rsrc); 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__); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term chan_tup = term_alloc_tuple(3, &ctx->heap); term_put_tuple_element(chan_tup, 0, globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "$dac"))); term_put_tuple_element(chan_tup, 1, chan_obj);