File 0210-Fix-repeated-binary-segment-bug.patch of Package erlang
From caf507f88fe289a513bc2dfd5ede4d3f9d104f89 Mon Sep 17 00:00:00 2001
From: Nelson Vides <videsnelson@gmail.com>
Date: Sat, 24 Jan 2026 15:38:49 +0100
Subject: [PATCH] Fix repeated binary segment bug
The following snippet reads garbage memory and even often triggers
segfaults. It barely ever happens with small Ns, but for around larger
than 40 it is almost guaranteed to segfault in my machine.
```erl
generate_max_length_names(N) ->
Label = <<<<$a>> || _ <- lists:seq(1, N)>>,
<<Label/binary, Label/binary, Label/binary>>.
```
I found this while trying to write some tests for bencharking dns packet
parsing logic. It did not trigger with only two labels, but with three
it is enough. These labels trigger or fail to trigger too:
```erl
BadLabel = <<<<$a>> || <<_>> <= <<1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1>> >>,
GoodLabel = <<1, 1, 1, 1, 1,1, 1, 1, 1, 1,1, 1, 1, 1, 1,1, 1, 1, 1, 1,1, 1, 1, 1, 1>>,
GoodLabel = iolist_to_binary(lists:duplicate(63, <<$a>>)),
```
My findings:
When the same binary variable is used multiple times in a single
bs_create_bin instruction (e.g., `<<Label/binary, Label/binary,
Label/binary>>`), the `private_append` optimization was incorrectly
applied. This caused memory corruption because:
1. The first segment uses `private_append` to destructively extend
the binary
2. The `ErlSubBits` structure is modified to reflect the new size
3. Subsequent segments read this modified size, not the original
4. The runtime copies more bytes than the original contained,
reading uninitialized/garbage memory
The fix adds a check to ensure that before setting `first_fragment_dies`
to true (which enables `private_append`), we verify the first fragment
variable doesn't appear in any other segment of the instruction.
---
lib/compiler/src/beam_ssa_alias.erl | 23 ++++++++++++++++---
.../private_append.erl | 13 +++++++++++
2 files changed, 33 insertions(+), 3 deletions(-)
diff --git a/lib/compiler/src/beam_ssa_alias.erl b/lib/compiler/src/beam_ssa_alias.erl
index 4a317254cc..f16d4cddb1 100644
--- a/lib/compiler/src/beam_ssa_alias.erl
+++ b/lib/compiler/src/beam_ssa_alias.erl
@@ -866,10 +866,16 @@ aa_update_annotation1(ArgsStatus,
%% annotation now, instead of trying to reconstruct the
%% kill map during the later transform pass.
Anno = case {Op,Args} of
- {bs_create_bin,[#b_literal{val=append},_,Var|_]} ->
+ {bs_create_bin,[#b_literal{val=append},_,Var|Rest]} ->
%% For the private-append optimization we need to
- %% know if the first fragment dies.
- Anno1#{first_fragment_dies => dies_at(Var, I, AAS)};
+ %% know if the first fragment dies. Additionally, if
+ %% the first fragment variable is used in other
+ %% segments, it should not be considered as dying with
+ %% this instruction since the later uses would read
+ %% corrupted data after a destructive append.
+ Dies = dies_at(Var, I, AAS) andalso
+ not bs_create_bin_var_in_segments(Var, Rest),
+ Anno1#{first_fragment_dies => Dies};
{update_record,[_Hint,_Size,Src|_Updates]} ->
%% One of the requirements for valid destructive
%% record updates is that the source tuple dies
@@ -982,6 +988,17 @@ aa_alias_repeated_args([_|Args], SS, Seen) ->
aa_alias_repeated_args([], SS, _Seen) ->
SS.
+%% Check if Var appears anywhere in the remaining segment arguments
+%% of a bs_create_bin instruction. The Var is the first fragment source,
+%% and Rest contains all arguments after the first fragment source.
+%% If Var appears again in Rest, using private_append would be unsafe
+%% because the later uses would read corrupted data after the first
+%% destructive append.
+bs_create_bin_var_in_segments(#b_var{}=Var, Args) ->
+ lists:member(Var, Args);
+bs_create_bin_var_in_segments(#b_literal{}, _Args) ->
+ false.
+
%% Return the kill-set for the instruction defining Dst.
aa_killset_for_instr(Dst, #aas{caller=Caller,kills=Kills}) ->
{_LiveIns,KillMap,_PhiLiveIns} = map_get(Caller, Kills),
diff --git a/lib/compiler/test/beam_ssa_check_SUITE_data/private_append.erl b/lib/compiler/test/beam_ssa_check_SUITE_data/private_append.erl
index c67fc2d82d..c0e14dc24d 100644
--- a/lib/compiler/test/beam_ssa_check_SUITE_data/private_append.erl
+++ b/lib/compiler/test/beam_ssa_check_SUITE_data/private_append.erl
@@ -78,6 +78,7 @@
not_transformable13/1,
not_transformable14/0,
not_transformable15/2,
+ not_transformable16/1,
id/1,
@@ -996,6 +997,18 @@ not_transformable15(V, _) when V ->
not_transformable15(_, V) ->
id(ok) bor V.
+%% Check that we don't use private_append when the same binary variable
+%% is used multiple times in a single bs_create_bin instruction.
+%% Using private_append would corrupt the binary after the first use,
+%% causing the second and third appends to read garbage data.
+not_transformable16(N) ->
+%ssa% (_) when post_ssa_opt ->
+%ssa% A = bs_init_writable(_),
+%ssa% B = call(_, _, A),
+%ssa% _ = bs_create_bin(append, _, B, ...).
+ Label = << <<$a>> || _ <- lists:seq(1, N) >>,
+ <<Label/binary, Label/binary, Label/binary>>.
+
id(I) ->
I.
--
2.51.0