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:
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:
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.
binary_to_term/1acceptsRECORD_EXTterms with duplicate field namesDescribe the bug
In Erlang/OTP 29,
binary_to_term/1accepts aRECORD_EXTwhose field-namelist 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/1returns the duplicate names, and
records:get/2selects 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/1preserves all of the original value slots.
To Reproduce
Save the following as
m.erl:Compile it and start an Erlang shell:
A
RECORD_EXTwith one field decodes normally:With two copies of the field name, it decodes into a record containing both:
The public constructor rejects the equivalent duplicate field names:
The following binary gives the two
afields different values,1and2: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/1should reject aRECORD_EXTcontaining duplicate fieldnames (with
badarg), so that it cannot create a native record that violatesthe field-name uniqueness invariant enforced by
records:create/4.Affected versions
Reproduced with:
RECORD_EXTand native records were introduced in OTP 29, so earlier OTPreleases 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 encodedrecord already exist.