Skip to content

binary_to_term/1 accepts RECORD_EXT terms with duplicate field names #11398

Description

@potatosalad

binary_to_term/1 accepts RECORD_EXT terms with duplicate field names

Describe the bug

In Erlang/OTP 29, binary_to_term/1 accepts a RECORD_EXT whose field-name
list contains the same name more than once. This creates a native record with
duplicate fields, even though duplicate field names are invalid and are
rejected by records:create/4.

The resulting record is internally inconsistent. records:get_field_names/1
returns the duplicate names, and records:get/2 selects the first occurrence.
If the duplicate fields contain different values, formatting the record also
shows the first value for every occurrence, although term_to_binary/1
preserves all of the original value slots.

To Reproduce

Save the following as m.erl:

-module(m).

-record #r{a :: integer()}.
-export_record([r]).
-export([y/2]).

-type u32() :: 0..16#FFFFFFFF.
-define(is_u32(N),
        (is_integer(N) andalso (N) >= 0 andalso (N) =< 16#FFFFFFFF)).

-spec y(integer(), u32()) -> binary().
y(A, N) when is_integer(A) andalso ?is_u32(N) ->
    <<131, AValue/bytes>> = erlang:term_to_binary(A),
    <<
        131,
        %% RECORD_EXT, N fields, exported flag set
        $C,
        N:32,
        1:8,
        %% Module 'm' and record name 'r'
        $w, 1, "m",
        $w, 1, "r",
        %% N copies of field name 'a', followed by N copies of value A
        (binary:copy(<<$w, 1, "a">>, N))/bytes,
        (binary:copy(AValue, N))/bytes
    >>.

Compile it and start an Erlang shell:

$ erlc m.erl
$ erl -pa .

A RECORD_EXT with one field decodes normally:

1> B1 = m:y(1, 1).
<<131,67,0,0,0,1,1,119,1,109,119,1,114,119,1,97,97,1>>
2> binary_to_term(B1).
#m:r{a = 1}

With two copies of the field name, it decodes into a record containing both:

3> B2 = m:y(1, 2).
<<131,67,0,0,0,2,1,119,1,109,119,1,114,119,1,97,119,1,97,97,1,97,1>>
4> R2 = binary_to_term(B2).
#m:r{a = 1,a = 1}
5> records:get_field_names(R2).
[a,a]

The public constructor rejects the equivalent duplicate field names:

6> records:create(m, r, [{a, 1}, {a, 1}], #{is_exported => true}).
** exception error: bad argument
     in function  records:create/4

The following binary gives the two a fields different values, 1 and 2:

7> B12 = <<131,67,0,0,0,2,1,119,1,109,119,1,114,119,1,97,119,1,97,97,1,97,2>>.
<<131,67,0,0,0,2,1,119,1,109,119,1,114,119,1,97,119,1,97,97,1,97,2>>
8> R12 = binary_to_term(B12).
#m:r{a = 1,a = 1}
9> records:get_field_names(R12).
[a,a]
10> records:get(a, R12).
1
11> term_to_binary(R12) =:= B12.
true

Thus, name-based access and formatting use the first value, while the second
value remains in the record and is emitted when the record is encoded again.

Expected behavior

binary_to_term/1 should reject a RECORD_EXT containing duplicate field
names (with badarg), so that it cannot create a native record that violates
the field-name uniqueness invariant enforced by records:create/4.

Affected versions

Reproduced with:

  • Erlang/OTP 29.0.4

RECORD_EXT and native records were introduced in OTP 29, so earlier OTP
releases are not applicable.

Additional context

The behavior was reproduced on arm64 macOS 26.5.2. It is also present when
using binary_to_term(Binary, [safe]), provided that the atoms in the encoded
record already exist.

Metadata

Metadata

Assignees

Labels

bugIssue is reported as a bugteam:VMAssigned to OTP team VM

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions