diff --git a/.github/workflows/codeql-analysis.yaml b/.github/workflows/codeql-analysis.yaml index b6b744d623..0d821737c5 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,./code-queries/allocations-without-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 4725f987a9..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 + 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 630a73398c..35d73cabb1 100644 --- a/.github/workflows/pico-build.yaml +++ b/.github/workflows/pico-build.yaml @@ -92,7 +92,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,./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 aed84aeb81..5b67e460fd 100644 --- a/.github/workflows/stm32-build.yaml +++ b/.github/workflows/stm32-build.yaml @@ -78,7 +78,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,./code-queries/allocations-without-ensure-free.ql - name: Build shell: bash diff --git a/.github/workflows/wasm-build.yaml b/.github/workflows/wasm-build.yaml index 321b669f26..ae92ce1eab 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,./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 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/code-queries/allocations-without-ensure-free.ql b/code-queries/allocations-without-ensure-free.ql new file mode 100644 index 0000000000..14cf949527 --- /dev/null +++ b/code-queries/allocations-without-ensure-free.ql @@ -0,0 +1,244 @@ +/** + * 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") +} + +/** + * 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/doc/src/differences-with-beam.md b/doc/src/differences-with-beam.md index 952bb837c9..901e15ddbf 100644 --- a/doc/src/differences-with-beam.md +++ b/doc/src/differences-with-beam.md @@ -217,6 +217,11 @@ locking the VM. Ports are also executed by the schedulers and should return quickly. +AtomVM supports few `enif_` functions with the same names and signatures as BEAM, however +there could be some small differences. For example, AtomVM tries to handle out of memory +errors without crashing and `enif_make_resource` and `enif_make_resource_binary` can return +invalid terms instead of crashing. + ### BEAM file compatibility AtomVM can run BEAM files generated by `erlc` compiler from OTP21 to the latest master version, diff --git a/src/libAtomVM/erl_nif.h b/src/libAtomVM/erl_nif.h index 5b97197891..8976a39953 100644 --- a/src/libAtomVM/erl_nif.h +++ b/src/libAtomVM/erl_nif.h @@ -207,9 +207,13 @@ int enif_release_resource(void *resource); * usage confusion, users should rather call `term_from_resource` and should * not decrement the reference counter. * + * AtomVM implementation differs from BEAM as it can return an invalid term if + * there is an out of memory error. + * * @param env current environment * @param obj resource - * @return a new term representing the resource + * @return a new term representing the resource or an invalid term in case of + * out of memory error */ ERL_NIF_TERM enif_make_resource(ErlNifEnv *env, void *obj); @@ -225,6 +229,9 @@ ERL_NIF_TERM enif_make_resource(ErlNifEnv *env, void *obj); * resource. The resource destructor will only be called when all binaries * are garbage collected. * + * AtomVM implementation differs from BEAM as it can return an invalid term if + * there is an out of memory error. + * * @param env current environment * @param obj resource * @param data binary data to encapsulate diff --git a/src/libAtomVM/jit_stream_flash.c b/src/libAtomVM/jit_stream_flash.c index 65b9c13423..f90336446f 100644 --- a/src/libAtomVM/jit_stream_flash.c +++ b/src/libAtomVM/jit_stream_flash.c @@ -537,6 +537,9 @@ static term nif_jit_stream_flash_new(Context *ctx, int argc, term argv[]) term obj = enif_make_resource(erl_nif_env_from_context(ctx), js); enif_release_resource(js); // decrement refcount after enif_alloc_resource + if (term_is_invalid_term(obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } return obj; } diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index fab7dff066..972bd3f220 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -2929,6 +2929,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); @@ -2992,7 +2993,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]; @@ -3907,11 +3912,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. @@ -6237,6 +6237,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/libAtomVM/otp_socket.c b/src/libAtomVM/otp_socket.c index 63e25919a8..b18fba2c3f 100644 --- a/src/libAtomVM/otp_socket.c +++ b/src/libAtomVM/otp_socket.c @@ -637,12 +637,11 @@ static term nif_socket_open(Context *ctx, int argc, term argv[]) #endif rsrc_obj->buf_size = DEFAULT_BUFFER_SIZE; - 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); - } term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); enif_release_resource(rsrc_obj); // decrement refcount after enif_alloc_resource + if (term_is_invalid_term(obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(2) + REF_SIZE; if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { @@ -1816,6 +1815,9 @@ static term make_accepted_socket_term(Context *ctx, struct SocketResource *conn_ { term obj = enif_make_resource(erl_nif_env_from_context(ctx), conn_rsrc_obj); enif_release_resource(conn_rsrc_obj); // decrement refcount after enif_allocate_resource in make_accepted_socket_resource + if (term_is_invalid_term(obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } term socket_term = term_alloc_tuple(2, &ctx->heap); uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); @@ -1901,6 +1903,9 @@ static term nif_socket_accept(Context *ctx, int argc, term argv[]) term new_resource = enif_make_resource(erl_nif_env_from_context(ctx), conn_rsrc_obj); enif_release_resource(conn_rsrc_obj); // decrement refcount after enif_alloc_resource + if (term_is_invalid_term(new_resource)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(2) + REF_SIZE; if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &new_resource, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { diff --git a/src/libAtomVM/otp_ssl.c b/src/libAtomVM/otp_ssl.c index ee86b96657..2fc9e7086b 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__); @@ -237,6 +233,9 @@ static term nif_ssl_entropy_init(Context *ctx, int argc, term argv[]) } term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); enif_release_resource(rsrc_obj); // decrement refcount after enif_alloc_resource + if (term_is_invalid_term(obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } mbedtls_entropy_init(&rsrc_obj->context); @@ -249,10 +248,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__); @@ -260,6 +255,9 @@ static term nif_ssl_ctr_drbg_init(Context *ctx, int argc, term argv[]) } term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); enif_release_resource(rsrc_obj); // decrement refcount after enif_alloc_resource + if (term_is_invalid_term(obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } mbedtls_ctr_drbg_init(&rsrc_obj->context); @@ -301,10 +299,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__); @@ -312,6 +306,9 @@ static term nif_ssl_init(Context *ctx, int argc, term argv[]) } term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); enif_release_resource(rsrc_obj); // decrement refcount after enif_alloc_resource + if (term_is_invalid_term(obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } mbedtls_ssl_init(&rsrc_obj->context); @@ -354,10 +351,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__); @@ -365,6 +358,9 @@ static term nif_ssl_config_init(Context *ctx, int argc, term argv[]) } term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); enif_release_resource(rsrc_obj); // decrement refcount after enif_alloc_resource + if (term_is_invalid_term(obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } mbedtls_ssl_config_init(&rsrc_obj->config); diff --git a/src/libAtomVM/posix_nifs.c b/src/libAtomVM/posix_nifs.c index 252cc21b83..247d184e97 100644 --- a/src/libAtomVM/posix_nifs.c +++ b/src/libAtomVM/posix_nifs.c @@ -240,6 +240,9 @@ static term make_posix_fd_resource(Context *ctx, int fd) fd_obj->selecting_process_id = INVALID_PROCESS_ID; term obj = enif_make_resource(erl_nif_env_from_context(ctx), fd_obj); enif_release_resource(fd_obj); // decrement refcount after enif_alloc_resource + if (term_is_invalid_term(obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } return obj; } @@ -852,6 +855,9 @@ static term nif_atomvm_posix_opendir(Context *ctx, int argc, term argv[]) } term obj = enif_make_resource(erl_nif_env_from_context(ctx), dir_obj); enif_release_resource(dir_obj); // decrement refcount after enif_alloc_resource + if (term_is_invalid_term(obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } result = term_alloc_tuple(2, &ctx->heap); term_put_tuple_element(result, 0, OK_ATOM); term_put_tuple_element(result, 1, obj); diff --git a/src/libAtomVM/resources.c b/src/libAtomVM/resources.c index 0a793a714b..98fb3efe64 100644 --- a/src/libAtomVM/resources.c +++ b/src/libAtomVM/resources.c @@ -123,7 +123,7 @@ int enif_release_resource(void *resource) ERL_NIF_TERM enif_make_resource(ErlNifEnv *env, void *obj) { if (UNLIKELY(memory_erl_nif_env_ensure_free(env, TERM_BOXED_REFERENCE_RESOURCE_SIZE) != MEMORY_GC_OK)) { - AVM_ABORT(); + return term_invalid_term(); } return term_from_resource(obj, &env->heap); } @@ -633,8 +633,8 @@ const ErlNifResourceTypeInit resource_binary_resource_type_init = { ERL_NIF_TERM enif_make_resource_binary(ErlNifEnv *env, void *obj, const void *data, size_t size) { - if (UNLIKELY(memory_erl_nif_env_ensure_free(env, TERM_BOXED_REFERENCE_RESOURCE_SIZE) != MEMORY_GC_OK)) { - AVM_ABORT(); + if (UNLIKELY(memory_erl_nif_env_ensure_free(env, TERM_BOXED_REFC_BINARY_SIZE) != MEMORY_GC_OK)) { + return term_invalid_term(); } struct ResourceBinary *resource_binary = enif_alloc_resource(env->global->resource_binary_resource_type, sizeof(struct ResourceBinary)); resource_binary->managing_resource = refc_binary_from_data(obj); diff --git a/src/platforms/emscripten/src/lib/platform_nifs.c b/src/platforms/emscripten/src/lib/platform_nifs.c index ed0a75f871..4b8eb17fdb 100644 --- a/src/platforms/emscripten/src/lib/platform_nifs.c +++ b/src/platforms/emscripten/src/lib/platform_nifs.c @@ -684,6 +684,9 @@ static EM_BOOL html5api_touch_callback(int eventType, const EmscriptenTouchEvent } \ term resource_term = enif_make_resource(erl_nif_env_from_context(ctx), resource); \ enif_release_resource(resource); \ + if (term_is_invalid_term(resource_term)) { \ + RAISE_ERROR(OUT_OF_MEMORY_ATOM); \ + } \ if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(3), 1, &resource_term, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { \ RAISE_ERROR(OUT_OF_MEMORY_ATOM); \ } \ diff --git a/src/platforms/esp32/components/avm_builtins/adc_driver.c b/src/platforms/esp32/components/avm_builtins/adc_driver.c index 67eaa6ac3b..e32b63c695 100644 --- a/src/platforms/esp32/components/avm_builtins/adc_driver.c +++ b/src/platforms/esp32/components/avm_builtins/adc_driver.c @@ -343,13 +343,11 @@ 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 = enif_make_resource(erl_nif_env_from_context(ctx), unit_rsrc); enif_release_resource(unit_rsrc); // decrement refcount after enif_alloc_resource + if (term_is_invalid_term(unit_obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } // {ok, {'$adc', Unit :: resource(), ref()}} size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(3) + REF_SIZE; @@ -494,14 +492,11 @@ 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))) { - enif_release_resource(chan_rsrc); - ESP_LOGE(TAG, "failed to allocate memory for resource: %s:%i.", __FILE__, __LINE__); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - term chan_obj = enif_make_resource(erl_nif_env_from_context(ctx), chan_rsrc); enif_release_resource(chan_rsrc); // decrement refcount after enif_alloc_resource + if (term_is_invalid_term(chan_obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } // {ok, {'$adc', resource(), ref()}} size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(3) + REF_SIZE; diff --git a/src/platforms/esp32/components/avm_builtins/dac_driver.c b/src/platforms/esp32/components/avm_builtins/dac_driver.c index 5a3d0d4b44..6779767e10 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 @@ -110,12 +111,11 @@ static term nif_oneshot_new_channel_p(Context *ctx, int argc, term argv[]) chan_rsrc->handle = 0; - if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { - ESP_LOGE(TAG, "failed to allocate memory for resource: %s:%i.", __FILE__, __LINE__); - enif_release_resource(chan_rsrc); + ERL_NIF_TERM chan_obj = enif_make_resource(erl_nif_env_from_context(ctx), chan_rsrc); + enif_release_resource(chan_rsrc); + if (term_is_invalid_term(chan_obj)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - ERL_NIF_TERM chan_obj = enif_make_resource(erl_nif_env_from_context(ctx), chan_rsrc); const dac_oneshot_config_t config = { .chan_id = term_to_uint8(argv[0]) @@ -123,9 +123,12 @@ static term nif_oneshot_new_channel_p(Context *ctx, int argc, term argv[]) const esp_err_t err = dac_oneshot_new_channel(&config, &chan_rsrc->handle); - 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); diff --git a/src/platforms/esp32/components/avm_builtins/i2c_resource.c b/src/platforms/esp32/components/avm_builtins/i2c_resource.c index 246b8aa386..b635f4bec5 100644 --- a/src/platforms/esp32/components/avm_builtins/i2c_resource.c +++ b/src/platforms/esp32/components/avm_builtins/i2c_resource.c @@ -216,14 +216,11 @@ static term nif_i2c_open(Context *ctx, int argc, term argv[]) rsrc_obj->i2c_num = i2c_num; rsrc_obj->send_timeout_ms = send_timeout_ms_val; - if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { - i2c_driver_delete(i2c_num); - enif_release_resource(rsrc_obj); - ESP_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } term obj = enif_make_resource(erl_nif_env_from_context(ctx), rsrc_obj); enif_release_resource(rsrc_obj); + if (term_is_invalid_term(obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } // // Return result diff --git a/src/platforms/esp32/components/avm_builtins/storage_nif.c b/src/platforms/esp32/components/avm_builtins/storage_nif.c index f12df62821..0d8bbd5b2e 100644 --- a/src/platforms/esp32/components/avm_builtins/storage_nif.c +++ b/src/platforms/esp32/components/avm_builtins/storage_nif.c @@ -266,6 +266,10 @@ static term nif_esp_mount(Context *ctx, int argc, term argv[]) RAISE_ERROR(OUT_OF_MEMORY_ATOM); } term mount_term = enif_make_resource(erl_nif_env_from_context(ctx), mount); + if (term_is_invalid_term(mount_term)) { + enif_release_resource(mount); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } return_term = term_alloc_tuple(2, &ctx->heap); term_put_tuple_element(return_term, 0, OK_ATOM); term_put_tuple_element(return_term, 1, mount_term); diff --git a/src/platforms/esp32/components/avm_sys/platform_nifs.c b/src/platforms/esp32/components/avm_sys/platform_nifs.c index 473c2fa033..6fbdc8727b 100644 --- a/src/platforms/esp32/components/avm_sys/platform_nifs.c +++ b/src/platforms/esp32/components/avm_sys/platform_nifs.c @@ -327,6 +327,9 @@ static term nif_esp_partition_mmap(Context *ctx, int argc, term argv[]) ErlNifEnv *env = erl_nif_env_from_context(ctx); ERL_NIF_TERM binary = enif_make_resource_binary(env, handle, mmap_ptr, size); enif_release_resource(handle); + if (term_is_invalid_term(binary)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &binary, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -707,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)); } @@ -829,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)); @@ -924,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); } // diff --git a/src/platforms/generic_unix/lib/jit_stream_mmap.c b/src/platforms/generic_unix/lib/jit_stream_mmap.c index 096cfe7faf..d8d5ce0ec2 100644 --- a/src/platforms/generic_unix/lib/jit_stream_mmap.c +++ b/src/platforms/generic_unix/lib/jit_stream_mmap.c @@ -91,6 +91,9 @@ static term nif_jit_stream_mmap_new(Context *ctx, int argc, term argv[]) term obj = enif_make_resource(erl_nif_env_from_context(ctx), js); enif_release_resource(js); // decrement refcount after enif_alloc_resource + if (term_is_invalid_term(obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } return obj; }