Skip to content

Make external term encoding and decoding iterative - #2316

Merged
bettio merged 1 commit into
atomvm:release-0.7from
pguyot:w21/external-term-iterative
Nov 4, 2026
Merged

bettio merged 1 commit into
atomvm:release-0.7from
pguyot:w21/external-term-iterative

Conversation

@pguyot

@pguyot pguyot commented May 24, 2026

Copy link
Copy Markdown
Collaborator

binary_to_term/1 and term_to_binary/1 decoded and encoded terms using unbounded C recursion, so a deeply nested term could exhaust the C stack. Observed on macOS which has a 512KB default.

Also reject LIST_EXT with no tail following OTP

These changes are made under both the "Apache 2.0" and the "GNU Lesser General
Public License 2.1 or later" license terms (dual license).

SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later

@pguyot
pguyot force-pushed the w21/external-term-iterative branch 2 times, most recently from e120095 to 0d41344 Compare May 24, 2026 18:12
@petermm

This comment was marked as outdated.

@pguyot
pguyot force-pushed the w21/external-term-iterative branch from 0d41344 to bbfe0c2 Compare May 24, 2026 20:44
@pguyot

pguyot commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author

Review item 1 is worth fixing in this PR, I amended the code.
I believe item 2 is overdefensive, max_pending is bounded by input length and malloc would fail anyway.

@pguyot
pguyot force-pushed the w21/external-term-iterative branch 6 times, most recently from 02d765d to acc9b54 Compare June 5, 2026 17:50
@pguyot
pguyot force-pushed the w21/external-term-iterative branch from acc9b54 to 088546a Compare June 8, 2026 19:49
@pguyot
pguyot force-pushed the w21/external-term-iterative branch from 088546a to 59991da Compare July 25, 2026 17:16
@pguyot
pguyot force-pushed the w21/external-term-iterative branch from 59991da to afb6f8d Compare September 25, 2026 15:46
@pguyot
pguyot force-pushed the w21/external-term-iterative branch from afb6f8d to daf8a79 Compare October 31, 2026 15:51
@petermm

This comment was marked as outdated.

@bettio bettio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest an improvement to the commit message.

The message reads as if deeply nested terms can no longer exhaust the C stack, but closures that capture closures still recurse (serialize_simple_term -> serialize_term, now with a TempStack frame per level). In a quick test with ulimit -s 512, encoding a fun() -> F end chain still crashes around depth 1750. Maybe narrow the wording to lists, tuples and maps, or mention that nested closures still recurse, so the message does not promise more than the diff does. The CHANGELOG entry on line 31 could use the same tweak.

Maybe we might add a TODO in order to make clear that there is still room for improvement from this point of view.

Comment thread CHANGELOG.md Outdated
- `erlang:term_to_binary/1,2` and `erlang:binary_to_term/1,2` now encode and decode terms
iteratively instead of with unbounded C recursion, so a deeply nested term no longer risks
exhausting the C stack
- `erlang:binary_to_term/1,2` now rejects a `LIST_EXT` with no tail (raising `badarg`), matching OTP

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already reject <<131,108,0,0,0,0>> with badarg.

What actually changes is that a zero-length LIST_EXT now decodes to its tail term like OTP 29: <<131,108,0,0,0,0,97,5>> gives 5 instead of [].

This also fixes the decoder consuming fewer bytes than the validator counted, so {LIST_EXT(0) 5, 6} decoded as {[],5} and now gives {5,6}.

Reword this entry (and let's move it to fixed) and the commit body's "Also reject LIST_EXT with no tail".

Nested lists, tuples and maps no longer recurse on the C stack.
Also decode a zero-length LIST_EXT to its tail, like OTP.

Signed-off-by: Paul Guyot <pguyot@kallisys.net>
@pguyot
pguyot force-pushed the w21/external-term-iterative branch from daf8a79 to 556a0ee Compare November 1, 2026 17:28

@bettio bettio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good for merge, let's fix in the future #2466

@bettio
bettio merged commit 5cb013f into atomvm:release-0.7 Nov 4, 2026
184 of 190 checks passed
bettio added a commit to bettio/AtomVM-fork that referenced this pull request Nov 5, 2026
Until atomvm#2316 the size pass of binary_to_term/1,2 accounted only for the
tag byte of a SMALL_TUPLE_EXT and not for its arity byte, so the number
of remaining bytes was over-counted by one for the whole tuple. An input
whose last element was missing its final byte passed validation and the
decoder read one byte past the end of the input, returning it as part of
the term: binary_to_term(<<131, 104, 2, 97, 1, 97>>) gave {1, X} with X
taken from whatever followed the buffer, where badarg is expected. With
a refc binary as input the read was outside the allocation.

The rewrite of the size pass in atomvm#2316 accounts for both header bytes,
but nothing in the suite exercised a truncated small tuple, so add the
cases that used to fail, along with boundary checks on the maximum
arity, nested empty tuples and trailing bytes with [used].

Signed-off-by: Davide Bettio <davide@uninstall.it>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants