[Pkg-erlang-devel] Bug#1108338: preapproval for unblock: erlang/1:27.3.4.1+dfsg-1 or erlang/1:27.3.4+dfsg-1 with a patch
Sergei Golovan
sgolovan at debian.org
Thu Jun 26 12:00:56 BST 2025
Package: release.debian.org
Severity: normal
X-Debbugs-Cc: erlang at packages.debian.org
Control: affects -1 + src:erlang
User: release.debian.org at packages.debian.org
Usertags: unblock
Hi!
I'd like to upload a fix for CVE-2025-4748 (insufficient path
sanitizing when extracting from ZIP apchives, see [1],[2] for details).
Upstream fix this bug in 27.3.4.1, but the changes include fixes for
several other bugs (27.3.4.1 is strictly a bugfix release). I'd like
to have these fixes in trixie as well.
So what would be better, to upload minimal changes which fix only
CVE-2025-4748, or the full 27.3.4.1?
I'm attaching both the full diff for 27.3.4.1 and separately the excerpt
from it concerning CVE-2025-4748.
[1] https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1107939
[2] https://security-tracker.debian.org/tracker/CVE-2025-4748
Cheers!
--
Sergei Golovan
-------------- next part --------------
diff -ruN otp-OTP-27.3.4/.github/dockerfiles/Dockerfile.ubuntu-base otp-OTP-27.3.4.1/.github/dockerfiles/Dockerfile.ubuntu-base
--- otp-OTP-27.3.4/.github/dockerfiles/Dockerfile.ubuntu-base 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/.github/dockerfiles/Dockerfile.ubuntu-base 2025-06-16 11:27:55.000000000 +0300
@@ -61,13 +61,19 @@
RUN mkdir /buildroot /tests /otp && chown ${USER}:${GROUP} /buildroot /tests /otp
+ARG LATEST_ERLANG_VERSION=unknown
+
## We install the latest version of the previous three releases in order to do
## backwards compatability testing of Erlang.
RUN apt-get update && apt-get install -y git curl && \
curl -L https://raw.githubusercontent.com/kerl/kerl/master/kerl > /usr/bin/kerl && \
chmod +x /usr/bin/kerl && \
kerl update releases && \
- LATEST=$(kerl list releases | grep "\*$" | tail -1 | awk -F '.' '{print $1}') && \
+ if [ ${LATEST_ERLANG_VERSION} = "unknown" ]; then \
+ LATEST=$(kerl list releases | grep "\*$" | tail -1 | awk -F '.' '{print $1}'); \
+ else \
+ LATEST=${LATEST_ERLANG_VERSION}; \
+ fi && \
for release in $(seq $(( LATEST - 2 )) $(( LATEST ))); do \
VSN=$(kerl list releases | grep "^$release" | tail -1 | awk '{print $1}'); \
if [ $release = $LATEST ]; then \
diff -ruN otp-OTP-27.3.4/.github/scripts/build-base-image.sh otp-OTP-27.3.4.1/.github/scripts/build-base-image.sh
--- otp-OTP-27.3.4/.github/scripts/build-base-image.sh 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/.github/scripts/build-base-image.sh 2025-06-16 11:27:55.000000000 +0300
@@ -21,10 +21,14 @@
set -eo pipefail
BASE_BRANCH="$1"
+LATEST_ERLANG_VERSION="unknown"
case "${BASE_BRANCH}" in
- master|maint|maint-*)
- ;;
+ maint-*)
+ LATEST_ERLANG_VERSION=${BASE_BRANCH#"maint-"}
+ ;;
+ master|maint)
+ ;;
*)
BASE_BRANCH="master"
;;
@@ -79,6 +83,7 @@
--build-arg MAKEFLAGS=-j6 \
--build-arg USER=otptest --build-arg GROUP=uucp \
--build-arg uid="$(id -u)" \
+ --build-arg LATEST_ERLANG_VERSION="${LATEST_ERLANG_VERSION}" \
--build-arg BASE="${BASE}" \
--build-arg BUILDKIT_INLINE_CACHE=1 \
.github/
diff -ruN otp-OTP-27.3.4/lib/asn1/doc/notes.md otp-OTP-27.3.4.1/lib/asn1/doc/notes.md
--- otp-OTP-27.3.4/lib/asn1/doc/notes.md 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/asn1/doc/notes.md 2025-06-16 11:27:55.000000000 +0300
@@ -21,6 +21,17 @@
This document describes the changes made to the asn1 application.
+## Asn1 5.3.4.1
+
+### Fixed Bugs and Malfunctions
+
+- The ASN.1 compiler could generate code that would cause Dialyzer with the `unmatched_returns` option to emit warnings.
+
+ Own Id: OTP-19638 Aux Id: [GH-9841], [PR-9846]
+
+[GH-9841]: https://github.com/erlang/otp/issues/9841
+[PR-9846]: https://github.com/erlang/otp/pull/9846
+
## Asn1 5.3.4
### Fixed Bugs and Malfunctions
diff -ruN otp-OTP-27.3.4/lib/asn1/src/asn1ct_gen_jer.erl otp-OTP-27.3.4.1/lib/asn1/src/asn1ct_gen_jer.erl
--- otp-OTP-27.3.4/lib/asn1/src/asn1ct_gen_jer.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/asn1/src/asn1ct_gen_jer.erl 2025-06-16 11:27:55.000000000 +0300
@@ -356,7 +356,7 @@
ok;
true ->
Args = [lists:concat(["element(",I,", Arg)"]) || I <- lists:seq(1, A)],
- emit([" ",{call,M,F,Args},com,nl])
+ emit([" _ = ",{call,M,F,Args},com,nl])
end.
%%===============================================================================
diff -ruN otp-OTP-27.3.4/lib/asn1/src/asn1ct_gen_per.erl otp-OTP-27.3.4.1/lib/asn1/src/asn1ct_gen_per.erl
--- otp-OTP-27.3.4/lib/asn1/src/asn1ct_gen_per.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/asn1/src/asn1ct_gen_per.erl 2025-06-16 11:27:55.000000000 +0300
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 1997-2024. All Rights Reserved.
+%% Copyright Ericsson AB 1997-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -60,7 +60,7 @@
Args =
[lists:concat(["element(",I,", Arg)"])
|| I <- lists:seq(1, A)],
- emit([" ",{call,M,F,Args},com,nl])
+ emit([" _ = ",{call,M,F,Args},com,nl])
end.
gen_encode(Erules,Type) when is_record(Type,typedef) ->
diff -ruN otp-OTP-27.3.4/lib/asn1/vsn.mk otp-OTP-27.3.4.1/lib/asn1/vsn.mk
--- otp-OTP-27.3.4/lib/asn1/vsn.mk 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/asn1/vsn.mk 2025-06-16 11:27:55.000000000 +0300
@@ -1 +1 @@
-ASN1_VSN = 5.3.4
+ASN1_VSN = 5.3.4.1
diff -ruN otp-OTP-27.3.4/lib/eldap/doc/notes.md otp-OTP-27.3.4.1/lib/eldap/doc/notes.md
--- otp-OTP-27.3.4/lib/eldap/doc/notes.md 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/eldap/doc/notes.md 2025-06-16 11:27:55.000000000 +0300
@@ -21,6 +21,16 @@
This document describes the changes made to the Eldap application.
+## Eldap 1.2.14.1
+
+### Fixed Bugs and Malfunctions
+
+- With this change eldap's 'not' function will have specs fixed.
+
+ Own Id: OTP-19658 Aux Id: [PR-9859]
+
+[PR-9859]: https://github.com/erlang/otp/pull/9859
+
## Eldap 1.2.14
### Fixed Bugs and Malfunctions
diff -ruN otp-OTP-27.3.4/lib/eldap/src/eldap.erl otp-OTP-27.3.4.1/lib/eldap/src/eldap.erl
--- otp-OTP-27.3.4/lib/eldap/src/eldap.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/eldap/src/eldap.erl 2025-06-16 11:27:55.000000000 +0300
@@ -775,7 +775,7 @@
Negate a filter.
""".
-doc(#{since => <<"OTP R15B01">>}).
--spec 'not'(Filter) -> filter() when Filter :: {filter()}.
+-spec 'not'(Filter) -> filter() when Filter :: filter().
'not'(Filter) when is_tuple(Filter) -> {'not',Filter}.
%%%
diff -ruN otp-OTP-27.3.4/lib/eldap/vsn.mk otp-OTP-27.3.4.1/lib/eldap/vsn.mk
--- otp-OTP-27.3.4/lib/eldap/vsn.mk 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/eldap/vsn.mk 2025-06-16 11:27:55.000000000 +0300
@@ -1 +1 @@
-ELDAP_VSN = 1.2.14
+ELDAP_VSN = 1.2.14.1
diff -ruN otp-OTP-27.3.4/lib/kernel/doc/notes.md otp-OTP-27.3.4.1/lib/kernel/doc/notes.md
--- otp-OTP-27.3.4/lib/kernel/doc/notes.md 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/kernel/doc/notes.md 2025-06-16 11:27:55.000000000 +0300
@@ -21,6 +21,24 @@
This document describes the changes made to the Kernel application.
+## Kernel 10.2.7.1
+
+### Fixed Bugs and Malfunctions
+
+- A remote shell can now exit by closing the input stream, without terminating the remote node.
+
+ Own Id: OTP-19667 Aux Id: [PR-9912]
+
+[PR-9912]: https://github.com/erlang/otp/pull/9912
+
+### Improvements and New Features
+
+- Document default buffer sizes
+
+ Own Id: OTP-19640 Aux Id: [GH-9722]
+
+[GH-9722]: https://github.com/erlang/otp/issues/9722
+
## Kernel 10.2.7
### Fixed Bugs and Malfunctions
diff -ruN otp-OTP-27.3.4/lib/kernel/src/inet.erl otp-OTP-27.3.4.1/lib/kernel/src/inet.erl
--- otp-OTP-27.3.4/lib/kernel/src/inet.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/kernel/src/inet.erl 2025-06-16 11:27:55.000000000 +0300
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 1997-2024. All Rights Reserved.
+%% Copyright Ericsson AB 1997-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -998,6 +998,9 @@
single recv call. If you are using higher than normal MTU consider setting
buffer higher.
+ For SCTP, defaults to 65536.
+ For TCP and UDP, defaults to 1460.
+
- **`{delay_send,?Boolean}`** - Normally, when an Erlang process sends to a
socket, the driver tries to send the data immediately. If that fails, the
driver uses any means available to queue up the message to be sent whenever
@@ -1336,6 +1339,9 @@
You are encouraged to use `getopts/2` to retrieve the size
set by your operating system.
+ For SCTP, defaults to 1024.
+ For UDP, defaults to 8K.
+
- **`{recvtclass,?Boolean}`** [](){: #option-recvtclass } -
If set to `true` activates returning the received `TCLASS` value
on platforms that implements the protocol `IPPROTO_IPV6` option
@@ -1493,6 +1499,8 @@
You are encouraged to use `getopts/2`, to retrieve the size
set by your operating system.
+ For SCTP, defaults to 65536.
+
- **`{priority,?Integer}`** - Sets the `SO_PRIORITY` socket level option on
platforms where this is implemented. The behavior and allowed range varies
between different systems. The option is ignored on platforms where it is not
diff -ruN otp-OTP-27.3.4/lib/kernel/src/kernel.appup.src otp-OTP-27.3.4.1/lib/kernel/src/kernel.appup.src
--- otp-OTP-27.3.4/lib/kernel/src/kernel.appup.src 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/kernel/src/kernel.appup.src 2025-06-16 11:27:55.000000000 +0300
@@ -43,6 +43,7 @@
{<<"^10\\.2\\.4(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
{<<"^10\\.2\\.5(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
{<<"^10\\.2\\.6(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
+ {<<"^10\\.2\\.7(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
{<<"^8\\.4$">>,[restart_new_emulator]},
{<<"^8\\.4\\.0(?:\\.[0-9]+)+$">>,[restart_new_emulator]},
{<<"^8\\.4\\.1(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
@@ -80,6 +81,7 @@
{<<"^10\\.2\\.4(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
{<<"^10\\.2\\.5(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
{<<"^10\\.2\\.6(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
+ {<<"^10\\.2\\.7(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
{<<"^8\\.4$">>,[restart_new_emulator]},
{<<"^8\\.4\\.0(?:\\.[0-9]+)+$">>,[restart_new_emulator]},
{<<"^8\\.4\\.1(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
diff -ruN otp-OTP-27.3.4/lib/kernel/src/user_drv.erl otp-OTP-27.3.4.1/lib/kernel/src/user_drv.erl
--- otp-OTP-27.3.4/lib/kernel/src/user_drv.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/kernel/src/user_drv.erl 2025-06-16 11:27:55.000000000 +0300
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 1996-2024. All Rights Reserved.
+%% Copyright Ericsson AB 1996-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -103,7 +103,7 @@
-record(editor, { port :: port(), file :: file:name(), requester :: pid() }).
-record(state, { tty :: prim_tty:state() | undefined,
write :: reference() | undefined,
- read :: reference() | undefined,
+ read :: reference() | eof | undefined,
shell_started = new :: new | old | false,
editor :: #editor{} | undefined,
user :: pid(),
@@ -443,7 +443,7 @@
end;
server(info, {ReadHandle,eof}, State = #state{ read = ReadHandle }) ->
State#state.current_group ! {self(), eof},
- {keep_state, State#state{ read = undefined }};
+ {keep_state, State#state{ read = eof }};
server(info,{ReadHandle,{signal,Signal}}, State = #state{ tty = TTYState, read = ReadHandle }) ->
{keep_state, State#state{ tty = prim_tty:handle_signal(TTYState, Signal) }};
@@ -567,10 +567,25 @@
current_group = NewGroup,
groups = Gr2 }};
_ -> % remote shell
- NewTTYState = io_requests(
- Reqs ++ [{put_chars,unicode,<<"(^G to start new job) ***\n">>}],
- State#state.tty),
- {keep_state, State#state{ tty = NewTTYState, groups = Gr1 }}
+ %% If the readhandle has terminated, then we should quit
+ case State#state.read =:= eof of
+ true ->
+ NewTTYState = io_requests(Reqs,
+ State#state.tty),
+ _ = io_request({put_chars_sync,unicode,<<"Read EOF ***\n">>, {self(), none}}, NewTTYState),
+ WriterRef = State#state.write,
+ receive
+ {WriterRef, ok} -> ok
+ after 100 ->
+ ok
+ end,
+ erlang:halt(0, []);
+ false ->
+ NewTTYState = io_requests(
+ Reqs ++ [{put_chars,unicode,<<"(^G to start new job) ***\n">>}],
+ State#state.tty),
+ {keep_state, State#state{ tty = NewTTYState, groups = Gr1 }}
+ end
end;
_ ->
{keep_state, State#state{ groups = gr_del_pid(State#state.groups, Group) }}
@@ -746,10 +761,16 @@
switch_cmd({r, Node, shell}, Gr);
switch_cmd({r,Node,Shell}, Gr0) when is_atom(Node), is_atom(Shell) ->
case is_alive() of
- true ->
- Pid = group:start(self(), {Node,Shell,start,[]}, group_opts(Node)),
- Gr = gr_add_cur(Gr0, Pid, {Node,Shell,start,[]}),
- {retry, [], Gr};
+ true ->
+ case net_kernel:connect_node(Node) of
+ true ->
+ Pid = group:start(self(), {Node,Shell,start,[]}, group_opts(Node)),
+ Gr = gr_add_cur(Gr0, Pid, {Node,Shell,start,[]}),
+ {retry, [], Gr};
+ false ->
+ Bin = atom_to_binary(Node),
+ {retry, [{put_chars,unicode,<<"Could not connect to node ", Bin/binary, "\n">>}]}
+ end;
false ->
{retry, [{put_chars,unicode,"Node is not alive\n"}]}
end;
diff -ruN otp-OTP-27.3.4/lib/kernel/test/interactive_shell_SUITE.erl otp-OTP-27.3.4.1/lib/kernel/test/interactive_shell_SUITE.erl
--- otp-OTP-27.3.4/lib/kernel/test/interactive_shell_SUITE.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/kernel/test/interactive_shell_SUITE.erl 2025-06-16 11:27:55.000000000 +0300
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2007-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2007-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -62,6 +62,7 @@
shell_standard_error_nlcr/1, shell_clear/1,
shell_format/1, shell_help/1,
remsh_basic/1, remsh_error/1, remsh_longnames/1, remsh_no_epmd/1,
+ remsh_dont_terminate_remote/1,
remsh_expand_compatibility_25/1, remsh_expand_compatibility_later_version/1,
external_editor/1, external_editor_visual/1,
external_editor_unicode/1, shell_ignore_pager_commands/1]).
@@ -107,6 +108,7 @@
remsh_error,
remsh_longnames,
remsh_no_epmd,
+ remsh_dont_terminate_remote,
remsh_expand_compatibility_25,
remsh_expand_compatibility_later_version]},
{tty,[],
@@ -2614,7 +2616,18 @@
%% Test that if we cannot connect to a node, we get a correct error
remsh_error(_Config) ->
"Could not connect to \"invalid_node\"\n" =
- os:cmd(ct:get_progname() ++ " -remsh invalid_node").
+ os:cmd(ct:get_progname() ++ " -remsh invalid_node"),
+
+ RemNode = peer:random_name(remsh_error),
+
+ rtnode:run([
+ {putdata, "\^g"},
+ {expect, " --> $"},
+ {putline, "r invalid_node"},
+ {expect, "Could not connect to node invalid_node"},
+ {expect, "--> $"}], RemNode),
+
+ ok.
quit_hosting_node() ->
%% Command sequence for entering a shell on the hosting node.
@@ -2626,6 +2639,31 @@
{expect, ["Eshell"]},
{expect, ["1> $"]}].
+remsh_dont_terminate_remote(Config) when is_list(Config) ->
+ {ok, Peer, TargetNode} = ?CT_PEER(),
+ TargetNodeStr = printed_atom(TargetNode),
+ [_Name,Host] = string:split(atom_to_list(node()), "@"),
+
+ %% Test that remsh works with explicit -sname.
+ HostNode = atom_to_list(?FUNCTION_NAME) ++ "_host",
+ %% Start a remote shell that will terminate because of an end of file
+ FullCmd = "erl -sname " ++ HostNode ++
+ " -remsh " ++ TargetNodeStr ++
+ " < /dev/null",
+ ct:log("~ts",[FullCmd]),
+ Output = os:cmd(FullCmd),
+ match = re:run(Output, "Shell process terminated! Read EOF", [{capture, none}]),
+
+ %% Start another remote shell, make sure the remote node has not terminated
+ rtnode:run([{putline, "node()."},
+ {expect, "\\Q" ++ TargetNodeStr ++ "\\E\r\n"}] ++
+ quit_hosting_node(),
+ HostNode, " ", "-remsh " ++ TargetNodeStr),
+
+ peer:stop(Peer),
+
+ ok.
+
%% Test that -remsh works with long names.
remsh_longnames(Config) when is_list(Config) ->
%% If we cannot resolve the domain, we need to add localhost to the longname
diff -ruN otp-OTP-27.3.4/lib/kernel/test/multi_load_SUITE.erl otp-OTP-27.3.4.1/lib/kernel/test/multi_load_SUITE.erl
--- otp-OTP-27.3.4/lib/kernel/test/multi_load_SUITE.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/kernel/test/multi_load_SUITE.erl 2025-06-16 11:27:55.000000000 +0300
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 1999-2021. All Rights Reserved.
+%% Copyright Ericsson AB 1999-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -192,16 +192,21 @@
fun(_) ->
hanging_on_load_module(Mod)
end),
- spawn_link(fun() ->
- {error,on_load_failure} =
- code:load_binary(Mod, Name, Bin)
- end).
+ register(spawn_hanging_on_load, self()),
+ Pid = spawn_link(fun() ->
+ {error,on_load_failure} =
+ code:load_binary(Mod, Name, Bin)
+ end),
+ receive hanging_on_load -> ok end,
+ unregister(spawn_hanging_on_load),
+ Pid.
hanging_on_load_module(Mod) ->
?Q(["-module('@Mod@').\n",
"-on_load(hang/0).\n",
"hang() ->\n"
" register(hanging_on_load, self()),\n"
+ " spawn_hanging_on_load ! hanging_on_load,\n"
" receive _ -> unload end.\n"]).
ensure_modules_loaded(Config) ->
diff -ruN otp-OTP-27.3.4/lib/kernel/vsn.mk otp-OTP-27.3.4.1/lib/kernel/vsn.mk
--- otp-OTP-27.3.4/lib/kernel/vsn.mk 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/kernel/vsn.mk 2025-06-16 11:27:55.000000000 +0300
@@ -1 +1 @@
-KERNEL_VSN = 10.2.7
+KERNEL_VSN = 10.2.7.1
diff -ruN otp-OTP-27.3.4/lib/ssh/doc/notes.md otp-OTP-27.3.4.1/lib/ssh/doc/notes.md
--- otp-OTP-27.3.4/lib/ssh/doc/notes.md 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/ssh/doc/notes.md 2025-06-16 11:27:55.000000000 +0300
@@ -19,6 +19,23 @@
-->
# SSH Release Notes
+## Ssh 5.2.11.1
+
+### Fixed Bugs and Malfunctions
+
+- Various channel closing robustness improvements. Avoid crashes when channel handling process closes channel and immediately exits. Avoid breaking the protocol by sending duplicated channel-close messages. Cleanup channels which timeout during closing procedure.
+
+ Own Id: OTP-19634 Aux Id: [GH-9102], [PR-9103]
+
+- Improved interoperability with clients acting as Paramiko.
+
+ Own Id: OTP-19637 Aux Id: [GH-6463], [PR-9838]
+
+[GH-9102]: https://github.com/erlang/otp/issues/9102
+[PR-9103]: https://github.com/erlang/otp/pull/9103
+[GH-6463]: https://github.com/erlang/otp/issues/6463
+[PR-9838]: https://github.com/erlang/otp/pull/9838
+
## Ssh 5.2.11
### Fixed Bugs and Malfunctions
diff -ruN otp-OTP-27.3.4/lib/ssh/src/ssh_connection.erl otp-OTP-27.3.4.1/lib/ssh/src/ssh_connection.erl
--- otp-OTP-27.3.4/lib/ssh/src/ssh_connection.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/ssh/src/ssh_connection.erl 2025-06-16 11:27:55.000000000 +0300
@@ -783,17 +783,26 @@
maximum_packet_size = PacketSz},
#connection{channel_cache = Cache} = Connection0, _, _SSH) ->
- #channel{remote_id = undefined} = Channel =
+ #channel{remote_id = undefined, user = U} = Channel =
ssh_client_channel:cache_lookup(Cache, ChannelId),
- ssh_client_channel:cache_update(Cache, Channel#channel{
- remote_id = RemoteId,
- recv_packet_size = max(32768, % rfc4254/5.2
- min(PacketSz, Channel#channel.recv_packet_size)
- ),
- send_window_size = WindowSz,
- send_packet_size = PacketSz}),
- reply_msg(Channel, Connection0, {open, ChannelId});
+ if U /= undefined ->
+ ssh_client_channel:cache_update(Cache, Channel#channel{
+ remote_id = RemoteId,
+ recv_packet_size = max(32768, % rfc4254/5.2
+ min(PacketSz, Channel#channel.recv_packet_size)
+ ),
+ send_window_size = WindowSz,
+ send_packet_size = PacketSz}),
+ reply_msg(Channel, Connection0, {open, ChannelId});
+ true ->
+ %% There is no user process so nobody cares about the channel
+ %% close it and remove from the cache, reply from the peer will be
+ %% ignored
+ CloseMsg = channel_close_msg(RemoteId),
+ ssh_client_channel:cache_delete(Cache, ChannelId),
+ {[{connection_reply, CloseMsg}], Connection0}
+ end;
handle_msg(#ssh_msg_channel_open_failure{recipient_channel = ChannelId,
reason = Reason,
@@ -842,6 +851,10 @@
{Replies, Connection};
undefined ->
+ %% This may happen among other reasons
+ %% - we sent 'channel-close' %% and the peer failed to respond in time
+ %% - we tried to open a channel but the handler died prematurely
+ %% and the channel entry was removed from the cache
{[], Connection0}
end;
@@ -1057,14 +1070,24 @@
?DEC_BIN(Err, _ErrLen),
?DEC_BIN(Lang, _LangLen)>> = Data,
case ssh_client_channel:cache_lookup(Cache, ChannelId) of
- #channel{remote_id = RemoteId} = Channel ->
+ #channel{remote_id = RemoteId, sent_close = SentClose} = Channel ->
{Reply, Connection} = reply_msg(Channel, Connection0,
{exit_signal, ChannelId,
binary_to_list(SigName),
binary_to_list(Err),
binary_to_list(Lang)}),
- ChannelCloseMsg = channel_close_msg(RemoteId),
- {[{connection_reply, ChannelCloseMsg}|Reply], Connection};
+ %% Send 'channel-close' only if it has not been sent yet
+ %% by e.g. our side also closing the channel or going down
+ %% and(!) update the cache
+ %% so that the 'channel-close' is not sent twice
+ if not SentClose ->
+ CloseMsg = channel_close_msg(RemoteId),
+ ssh_client_channel:cache_update(Cache,
+ Channel#channel{sent_close = true}),
+ {[{connection_reply, CloseMsg}|Reply], Connection};
+ true ->
+ {Reply, Connection}
+ end;
_ ->
%% Channel already closed by peer
{[], Connection0}
diff -ruN otp-OTP-27.3.4/lib/ssh/src/ssh_connection_handler.erl otp-OTP-27.3.4.1/lib/ssh/src/ssh_connection_handler.erl
--- otp-OTP-27.3.4/lib/ssh/src/ssh_connection_handler.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/ssh/src/ssh_connection_handler.erl 2025-06-16 11:27:55.000000000 +0300
@@ -686,9 +686,12 @@
{stop, Shutdown, D};
-%%% ######## {service_request, client|server} ####
-
-handle_event(internal, Msg = #ssh_msg_service_request{name=ServiceName}, StateName = {service_request,server}, D0) ->
+%%% ######## {service_request, client|server} #### StateName ==
+%% {userauth,server} guard added due to interoperability with clients
+%% sending extra ssh_msg_service_request (e.g. Paramiko for Python,
+%% see GH-6463)
+handle_event(internal, Msg = #ssh_msg_service_request{name=ServiceName}, StateName, D0)
+ when StateName == {service_request,server}; StateName == {userauth,server} ->
case ServiceName of
"ssh-userauth" ->
Ssh0 = #ssh{session_id=SessionId} = D0#data.ssh_params,
@@ -1089,12 +1092,22 @@
handle_event({call,From}, {close, ChannelId}, StateName, D0)
when ?CONNECTED(StateName) ->
+ %% Send 'channel-close' only if it has not been sent yet
+ %% e.g. when 'exit-signal' was received from the peer
+ %% and(!) we update the cache so that we remember what we've done
case ssh_client_channel:cache_lookup(cache(D0), ChannelId) of
- #channel{remote_id = Id} = Channel ->
+ #channel{remote_id = Id, sent_close = false} = Channel ->
D1 = send_msg(ssh_connection:channel_close_msg(Id), D0),
- ssh_client_channel:cache_update(cache(D1), Channel#channel{sent_close = true}),
- {keep_state, D1, [cond_set_idle_timer(D1), {reply,From,ok}]};
- undefined ->
+ ssh_client_channel:cache_update(cache(D1),
+ Channel#channel{sent_close = true}),
+ {keep_state, D1, [cond_set_idle_timer(D1),
+ channel_close_timer(D1, Id),
+ {reply,From,ok}]};
+ _ ->
+ %% Here we match a channel which has already sent 'channel-close'
+ %% AND possible cases of 'broken cache' i.e. when a channel
+ %% disappeared from the cache, but has not been properly shut down
+ %% The latter would be a bug, but hard to chase
{keep_state_and_data, [{reply,From,ok}]}
end;
@@ -1255,15 +1268,33 @@
%%% Handle that ssh channels user process goes down
handle_event(info, {'DOWN', _Ref, process, ChannelPid, _Reason}, _, D) ->
Cache = cache(D),
- ssh_client_channel:cache_foldl(
- fun(#channel{user=U,
- local_id=Id}, Acc) when U == ChannelPid ->
- ssh_client_channel:cache_delete(Cache, Id),
- Acc;
- (_,Acc) ->
- Acc
- end, [], Cache),
- {keep_state, D, cond_set_idle_timer(D)};
+ %% Here we first collect the list of channel id's handled by the process
+ %% Do NOT remove them from the cache - they are not closed yet!
+ Channels = ssh_client_channel:cache_foldl(
+ fun(#channel{user=U} = Channel, Acc) when U == ChannelPid ->
+ [Channel | Acc];
+ (_,Acc) ->
+ Acc
+ end, [], Cache),
+ %% Then for each channel where 'channel-close' has not been sent yet
+ %% we send 'channel-close' and(!) update the cache so that we remember
+ %% what we've done.
+ %% Also set user as 'undefined' as there is no such process anyway
+ {D2, NewTimers} = lists:foldl(
+ fun(#channel{remote_id = Id, sent_close = false} = Channel,
+ {D0, Timers}) when Id /= undefined ->
+ D1 = send_msg(ssh_connection:channel_close_msg(Id), D0),
+ ssh_client_channel:cache_update(cache(D1),
+ Channel#channel{sent_close = true,
+ user = undefined}),
+ ChannelTimer = channel_close_timer(D1, Id),
+ {D1, [ChannelTimer | Timers]};
+ (Channel, {D0, _} = Acc) ->
+ ssh_client_channel:cache_update(cache(D0),
+ Channel#channel{user = undefined}),
+ Acc
+ end, {D, []}, Channels),
+ {keep_state, D2, [cond_set_idle_timer(D2) | NewTimers]};
handle_event({timeout,idle_time}, _Data, _StateName, D) ->
case ssh_client_channel:cache_info(num_entries, cache(D)) of
@@ -1276,6 +1307,16 @@
handle_event({timeout,max_initial_idle_time}, _Data, _StateName, _D) ->
{stop, {shutdown, "Timeout"}};
+handle_event({timeout, {channel_close, ChannelId}}, _Data, _StateName, D) ->
+ Cache = cache(D),
+ case ssh_client_channel:cache_lookup(Cache, ChannelId) of
+ #channel{sent_close = true} ->
+ ssh_client_channel:cache_delete(Cache, ChannelId),
+ {keep_state, D, cond_set_idle_timer(D)};
+ _ ->
+ keep_state_and_data
+ end;
+
%%% So that terminate will be run when supervisor is shutdown
handle_event(info, {'EXIT', _Sup, Reason}, StateName, _D) ->
Role = ?role(StateName),
@@ -2048,6 +2089,10 @@
_ -> {{timeout,idle_time}, infinity, none}
end.
+channel_close_timer(D, ChannelId) ->
+ {{timeout, {channel_close, ChannelId}},
+ ?GET_OPT(channel_close_timeout, (D#data.ssh_params)#ssh.opts), none}.
+
%%%----------------------------------------------------------------
start_channel_request_timer(_,_, infinity) ->
ok;
diff -ruN otp-OTP-27.3.4/lib/ssh/src/ssh_options.erl otp-OTP-27.3.4.1/lib/ssh/src/ssh_options.erl
--- otp-OTP-27.3.4/lib/ssh/src/ssh_options.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/ssh/src/ssh_options.erl 2025-06-16 11:27:55.000000000 +0300
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -886,6 +886,12 @@
#{default => ?MAX_RND_PADDING_LEN,
chk => fun(V) -> check_non_neg_integer(V) end,
class => undoc_user_option
+ },
+
+ channel_close_timeout =>
+ #{default => 5 * 1000,
+ chk => fun(V) -> check_non_neg_integer(V) end,
+ class => undoc_user_option
}
}.
diff -ruN otp-OTP-27.3.4/lib/ssh/test/ssh_connection_SUITE.erl otp-OTP-27.3.4.1/lib/ssh/test/ssh_connection_SUITE.erl
--- otp-OTP-27.3.4/lib/ssh/test/ssh_connection_SUITE.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/ssh/test/ssh_connection_SUITE.erl 2025-06-16 11:27:55.000000000 +0300
@@ -109,6 +109,7 @@
stop_listener/1,
trap_exit_connect/1,
trap_exit_daemon/1,
+ handler_down_before_open/1,
ssh_exec_echo/2 % called as an MFA
]).
@@ -180,7 +181,8 @@
stop_listener,
no_sensitive_leak,
start_subsystem_on_closed_channel,
- max_channels_option
+ max_channels_option,
+ handler_down_before_open
].
groups() ->
[{openssh, [], payload() ++ ptty() ++ sock()}].
@@ -1294,7 +1296,7 @@
do_start_shell_exec_fun(Fun, Command, Expect, ExpectType, Config) ->
DefaultReceiveFun =
- fun(ConnectionRef, ChannelId, Expect, ExpectType) ->
+ fun(ConnectionRef, ChannelId, _Expect, _ExpectType) ->
receive
{ssh_cm, ConnectionRef, {data, ChannelId, ExpectType, Expect}} ->
ok
@@ -1943,6 +1945,138 @@
ssh:close(ConnectionRef),
ssh:stop_daemon(Pid).
+handler_down_before_open(Config) ->
+ %% Start echo subsystem with a delay in init() - until a signal is received
+ %% One client opens a channel on the connection
+ %% the other client requests the echo subsystem on the second channel and then immediately goes down
+ %% the test monitors the client and when receiving 'DOWN' signals 'echo' to proceed
+ %% a) there should be no crash after 'channel-open-confirmation'
+ %% b) there should be proper 'channel-close' exchange
+ %% c) the 'exec' channel should not be affected after the 'echo' channel goes down
+ PrivDir = proplists:get_value(priv_dir, Config),
+ UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth
+ file:make_dir(UserDir),
+ SysDir = proplists:get_value(data_dir, Config),
+ Parent = self(),
+ EchoSS_spec = {ssh_echo_server, [8, [{dbg, true}, {parent, Parent}]]},
+ {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir},
+ {user_dir, UserDir},
+ {password, "morot"},
+ {exec, fun ssh_exec_echo/1},
+ {subsystems, [{"echo_n",EchoSS_spec}]}]),
+ ct:log("~p:~p connect", [?MODULE,?LINE]),
+ ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true},
+ {user, "foo"},
+ {password, "morot"},
+ {user_interaction, false},
+ {user_dir, UserDir}]),
+ ct:log("~p:~p connected", [?MODULE,?LINE]),
+
+ ExecChannelPid =
+ spawn(
+ fun() ->
+ {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity),
+
+ %% This is to get peer's connection handler PID ({conn_peer ...} below) and suspend it
+ {ok, ChannelId1} = ssh_connection:session_channel(ConnectionRef, infinity),
+ ssh_connection:subsystem(ConnectionRef, ChannelId1, "echo_n", infinity),
+ ssh_connection:close(ConnectionRef, ChannelId1),
+ receive
+ {ssh_cm, ConnectionRef, {closed, 1}} -> ok
+ end,
+
+ Parent ! {self(), channelId, ChannelId0},
+ Result = receive
+ cmd ->
+ ct:log("~p:~p Channel ~p executing", [?MODULE, ?LINE, ChannelId0]),
+ success = ssh_connection:exec(ConnectionRef, ChannelId0, "testing", infinity),
+ Expect = <<"echo testing\n">>,
+ ExpSz = size(Expect),
+ receive
+ {ssh_cm, ConnectionRef, {data, ChannelId0, 0,
+ <<Expect:ExpSz/binary, _/binary>>}} = R ->
+ ct:log("~p:~p Got expected ~p",[?MODULE,?LINE, R]),
+ ok;
+ Other ->
+ ct:log("~p:~p Got unexpected ~p~nExpect: ~p~n",
+ [?MODULE,?LINE, Other, {ssh_cm, ConnectionRef,
+ {data, ChannelId0, 0, Expect}}]),
+ {fail, "Unexpected data"}
+ after 5000 ->
+ {fail, "Exec Timeout"}
+ end;
+ stop -> {fail, "Stopped"}
+ end,
+ Parent ! {self(), Result}
+ end),
+ try
+ receive
+ {ExecChannelPid, channelId, ExId} ->
+ ct:log("~p:~p Channel that should stay: ~p pid ~p",
+ [?MODULE, ?LINE, ExId, ExecChannelPid]),
+ %% This is sent by the echo subsystem as a reaction to channel1 above
+ ConnPeer = receive {conn_peer, CM} -> CM end,
+ %% The sole purpose of this channel is to go down
+ %% before the opening procedure is complete
+ DownChannelPid = spawn(
+ fun() ->
+ ct:log("~p:~p open channel (incomplete)",[?MODULE,?LINE]),
+ Parent ! {self(), channelId, ok},
+ %% This is to prevent the peer from answering our 'channel-open' in time
+ sys:suspend(ConnPeer),
+ {ok, _} = ssh_connection:session_channel(ConnectionRef, infinity)
+ end),
+ MonRef = erlang:monitor(process, DownChannelPid),
+ receive
+ {DownChannelPid, channelId, ok} ->
+ ct:log("~p:~p Channel handler that won't continue: pid ~p",
+ [?MODULE, ?LINE, DownChannelPid]),
+ ensure_channels(ConnectionRef, 2),
+ channel_down_sequence(DownChannelPid, ExecChannelPid,
+ ExId, MonRef, ConnectionRef, ConnPeer)
+ end
+ end,
+ ensure_channels(ConnectionRef, 0)
+ after
+ ssh:close(ConnectionRef),
+ ssh:stop_daemon(Pid)
+ end.
+
+ensure_channels(ConnRef, Expected) ->
+ {ok, ChannelList} = ssh_connection_handler:info(ConnRef),
+ do_ensure_channels(ConnRef, Expected, length(ChannelList)).
+
+do_ensure_channels(_ConnRef, NumExpected, NumExpected) ->
+ ok;
+do_ensure_channels(ConnRef, NumExpected, _ChannelListLen) ->
+ ct:sleep(100),
+ {ok, ChannelList} = ssh_connection_handler:info(ConnRef),
+ do_ensure_channels(ConnRef, NumExpected, length(ChannelList)).
+
+channel_down_sequence(DownChannelPid, ExecChannelPid, ExecChannelId, MonRef, ConnRef, Peer) ->
+ ct:log("~p:~p sending order to ~p to go down", [?MODULE, ?LINE, DownChannelPid]),
+ exit(DownChannelPid, die),
+ receive {'DOWN', MonRef, _, _, _} -> ok end,
+ ct:log("~p:~p order executed, sending order to ~p to proceed", [?MODULE, ?LINE, Peer]),
+ %% Resume the peer connection to let it clean up among its channels
+ sys:resume(Peer),
+ ensure_channels(ConnRef, 1),
+ ExecChannelPid ! cmd,
+ try
+ receive
+ {ExecChannelPid, ok} ->
+ ct:log("~p:~p expected exec result: ~p", [?MODULE, ?LINE, ok]),
+ ok;
+ {ExecChannelPid, Result} ->
+ ct:log("~p:~p Unexpected exec result: ~p", [?MODULE, ?LINE, Result]),
+ {fail, "Unexpected exec result"}
+ after 5000 ->
+ {fail, "Exec result timeout"}
+ end
+ after
+ ssh_connection:close(ConnRef, ExecChannelId)
+ end.
+
%%--------------------------------------------------------------------
%% Internal functions ------------------------------------------------
%%--------------------------------------------------------------------
diff -ruN otp-OTP-27.3.4/lib/ssh/test/ssh_echo_server.erl otp-OTP-27.3.4.1/lib/ssh/test/ssh_echo_server.erl
--- otp-OTP-27.3.4/lib/ssh/test/ssh_echo_server.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/ssh/test/ssh_echo_server.erl 2025-06-16 11:27:55.000000000 +0300
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2005-2021. All Rights Reserved.
+%% Copyright Ericsson AB 2005-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -27,7 +27,8 @@
n,
id,
cm,
- dbg = false
+ dbg = false,
+ parent
}).
-export([init/1, handle_msg/2, handle_ssh_msg/2, terminate/2]).
@@ -42,13 +43,19 @@
{ok, #state{n = N}};
init([N,Opts]) ->
State = #state{n = N,
- dbg = proplists:get_value(dbg,Opts,false)
+ dbg = proplists:get_value(dbg,Opts,false),
+ parent = proplists:get_value(parent, Opts)
},
?DBG(State, "init([~p])",[N]),
{ok, State}.
handle_msg({ssh_channel_up, ChannelId, ConnectionManager}, State) ->
?DBG(State, "ssh_channel_up Cid=~p ConnMngr=~p",[ChannelId,ConnectionManager]),
+ Pid = State#state.parent,
+ if Pid /= undefined ->
+ Pid ! {conn_peer, ConnectionManager};
+ true -> ok
+ end,
{ok, State#state{id = ChannelId,
cm = ConnectionManager}}.
diff -ruN otp-OTP-27.3.4/lib/ssh/test/ssh_protocol_SUITE.erl otp-OTP-27.3.4.1/lib/ssh/test/ssh_protocol_SUITE.erl
--- otp-OTP-27.3.4/lib/ssh/test/ssh_protocol_SUITE.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/ssh/test/ssh_protocol_SUITE.erl 2025-06-16 11:27:55.000000000 +0300
@@ -26,6 +26,7 @@
-include_lib("kernel/include/inet.hrl").
-include("ssh.hrl"). % ?UINT32, ?BYTE, #ssh{} ...
-include("ssh_transport.hrl").
+-include("ssh_connect.hrl").
-include("ssh_auth.hrl").
-include("ssh_test_lib.hrl").
@@ -85,7 +86,9 @@
preferred_algorithms/1,
service_name_length_too_large/1,
service_name_length_too_short/1,
- client_close_after_hello/1
+ client_close_after_hello/1,
+ channel_close_timeout/1,
+ extra_ssh_msg_service_request/1
]).
-define(NEWLINE, <<"\r\n">>).
@@ -124,7 +127,8 @@
{group,field_size_error},
{group,ext_info},
{group,preferred_algorithms},
- {group,client_close_early}
+ {group,client_close_early},
+ {group,channel_close}
].
groups() ->
@@ -155,7 +159,8 @@
bad_long_service_name,
bad_very_long_service_name,
empty_service_name,
- bad_service_name_then_correct
+ bad_service_name_then_correct,
+ extra_ssh_msg_service_request
]},
{authentication, [], [client_handles_keyboard_interactive_0_pwds,
client_handles_banner_keyboard_interactive
@@ -171,8 +176,8 @@
modify_rm,
modify_combo
]},
- {client_close_early, [], [client_close_after_hello
- ]}
+ {client_close_early, [], [client_close_after_hello]},
+ {channel_close, [], [channel_close_timeout]}
].
@@ -1342,6 +1347,44 @@
{fail, no_handshakers}
end.
+%%% Connect to an erlang server and pretend client sending extra
+%%% ssh_msg_service_request (Paramiko client behavior)
+extra_ssh_msg_service_request(Config) ->
+ %% Connect and negotiate keys
+ {ok,InitialState} = ssh_trpt_test_lib:exec(
+ [{set_options, [print_ops, print_seqnums, print_messages]}]
+ ),
+ {ok,AfterKexState} = connect_and_kex(Config, InitialState),
+ %% Do the authentcation
+ {User,Pwd} = server_user_password(Config),
+ UserAuthFlow =
+ fun(P) ->
+ [{send, #ssh_msg_service_request{name = "ssh-userauth"}},
+ {match, #ssh_msg_service_accept{name = "ssh-userauth"}, receive_msg},
+ {send, #ssh_msg_userauth_request{user = User,
+ service = "ssh-connection",
+ method = "password",
+ data = <<?BOOLEAN(?FALSE),
+ ?STRING(unicode:characters_to_binary(P))>>
+ }}]
+ end,
+ {ok,EndState} =
+ ssh_trpt_test_lib:exec(
+ UserAuthFlow("WRONG") ++
+ [{match, #ssh_msg_userauth_failure{_='_'}, receive_msg}] ++
+ UserAuthFlow(Pwd) ++
+ [{match, #ssh_msg_userauth_success{_='_'}, receive_msg}],
+ AfterKexState),
+ %% Disconnect
+ {ok,_} =
+ ssh_trpt_test_lib:exec(
+ [{send, #ssh_msg_disconnect{code = ?SSH_DISCONNECT_BY_APPLICATION,
+ description = "End of the fun",
+ language = ""
+ }},
+ close_socket
+ ], EndState),
+ ok.
%%%================================================================
%%%==== Internal functions ========================================
@@ -1508,6 +1551,84 @@
],
InitialState).
+channel_close_timeout(Config) ->
+ {User,_Pwd} = server_user_password(Config),
+ %% Create a listening socket as server socket:
+ {ok,InitialState} = ssh_trpt_test_lib:exec(listen),
+ HostPort = ssh_trpt_test_lib:server_host_port(InitialState),
+ %% Start a process handling one connection on the server side:
+ spawn_link(
+ fun() ->
+ {ok,_} =
+ ssh_trpt_test_lib:exec(
+ [{set_options, [print_ops, print_messages]},
+ {accept, [{system_dir, system_dir(Config)},
+ {user_dir, user_dir(Config)},
+ {idle_time, 50000}]},
+ receive_hello,
+ {send, hello},
+ {send, ssh_msg_kexinit},
+ {match, #ssh_msg_kexinit{_='_'}, receive_msg},
+ {match, #ssh_msg_kexdh_init{_='_'}, receive_msg},
+ {send, ssh_msg_kexdh_reply},
+ {send, #ssh_msg_newkeys{}},
+ {match, #ssh_msg_newkeys{_='_'}, receive_msg},
+ {match, #ssh_msg_service_request{name="ssh-userauth"}, receive_msg},
+ {send, #ssh_msg_service_accept{name="ssh-userauth"}},
+ {match, #ssh_msg_userauth_request{service="ssh-connection",
+ method="none",
+ user=User,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_userauth_failure{authentications = "password",
+ partial_success = false}},
+ {match, #ssh_msg_userauth_request{service="ssh-connection",
+ method="password",
+ user=User,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_userauth_success{}},
+ {match, #ssh_msg_channel_open{channel_type="session",
+ sender_channel=0,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_channel_open_confirmation{recipient_channel= 0,
+ sender_channel = 0,
+ initial_window_size = 64*1024,
+ maximum_packet_size = 32*1024
+ }},
+ {match, #ssh_msg_channel_open{channel_type="session",
+ sender_channel=1,
+ _='_'}, receive_msg},
+ {send, #ssh_msg_channel_open_confirmation{recipient_channel= 1,
+ sender_channel = 1,
+ initial_window_size = 64*1024,
+ maximum_packet_size = 32*1024}},
+ {match, #ssh_msg_channel_close{recipient_channel = 0}, receive_msg},
+ {match, disconnect(), receive_msg},
+ print_state],
+ InitialState)
+ end),
+ %% connect to it with a regular Erlang SSH client:
+ ChannelCloseTimeout = 3000,
+ {ok, ConnRef} = std_connect(HostPort, Config,
+ [{preferred_algorithms,[{kex,[?DEFAULT_KEX]},
+ {cipher,?DEFAULT_CIPHERS}
+ ]},
+ {channel_close_timeout, ChannelCloseTimeout},
+ {idle_time, 50000}
+ ]
+ ),
+ {ok, Channel0} = ssh_connection:session_channel(ConnRef, 50000),
+ {ok, _Channel1} = ssh_connection:session_channel(ConnRef, 50000),
+ %% Close the channel from client side, the server does not reply with 'channel-close'
+ %% After the timeout, the client should drop the cache entry
+ _ = ssh_connection:close(ConnRef, Channel0),
+ receive
+ after ChannelCloseTimeout + 1000 ->
+ {channels, Channels} = ssh:connection_info(ConnRef, channels),
+ ct:log("Channel entries ~p", [Channels]),
+ %% Only one channel entry should be present, the other one should be dropped
+ 1 = length(Channels),
+ ssh:close(ConnRef)
+ end.
%%%----------------------------------------------------------------
%%% For matching peer disconnection
diff -ruN otp-OTP-27.3.4/lib/ssh/vsn.mk otp-OTP-27.3.4.1/lib/ssh/vsn.mk
--- otp-OTP-27.3.4/lib/ssh/vsn.mk 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/ssh/vsn.mk 2025-06-16 11:27:55.000000000 +0300
@@ -1,4 +1,4 @@
#-*-makefile-*- ; force emacs to enter makefile-mode
-SSH_VSN = 5.2.11
+SSH_VSN = 5.2.11.1
APP_VSN = "ssh-$(SSH_VSN)"
diff -ruN otp-OTP-27.3.4/lib/ssl/doc/guides/ssl_distribution.md otp-OTP-27.3.4.1/lib/ssl/doc/guides/ssl_distribution.md
--- otp-OTP-27.3.4/lib/ssl/doc/guides/ssl_distribution.md 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/ssl/doc/guides/ssl_distribution.md 2025-06-16 11:27:55.000000000 +0300
@@ -1,7 +1,7 @@
<!--
%CopyrightBegin%
-Copyright Ericsson AB 2023-2024. All Rights Reserved.
+Copyright Ericsson AB 2023-2025. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -214,6 +214,65 @@
A node started in this way is fully functional, using TLS as the distribution
protocol.
+## verify_fun Configuration Example
+
+The `verify_fun` option creates a reference to the implementing
+function since the configuration is evaluated as an Erlang term. In
+an example file for use with `-ssl_dist_optfile`:
+
+
+```erlang
+[{server,[{fail_if_no_peer_cert,true},
+ {certfile,"/home/me/ssl/cert.pem"},
+ {keyfile,"/home/me/ssl/privkey.pem"},
+ {cacertfile,"/home/me/ssl/ca_cert.pem"},
+ {verify,verify_peer},
+ {verify_fun,{fun mydist:verify/3,"any initial value"}}]},
+ {client,[{certfile,"/home/me/ssl/cert.pem"},
+ {keyfile,"/home/me/ssl/privkey.pem"},
+ {cacertfile,"/home/me/ssl/ca_cert.pem"},
+ {verify,verify_peer},
+ {verify_fun,{fun mydist:verify/3,"any initial value"}}]}].
+
+```
+
+`mydist:verify/3` will be called with:
+
+ * OtpCert, the other party's certificate [PKIX Certificates](`e:public_key:public_key_records.html#pkix-certificates`)
+ * SslStatus, OTP's verification outcome, such as `valid` or a tuple `{bad_cert, unknown_ca}`
+ * Init will be `"any initial value"`
+
+A pattern for `verify/3` will look like:
+
+```erlang
+verify(OtpCert, _SslStatus, Init) ->
+ IsOk = is_ok(OtpCert, Init),
+ NewInitValue = "some new value",
+ case IsOk of
+ true ->
+ {valid, NewInitValue};
+ false ->
+ {failure, NewInitValue}
+ end.
+```
+
+`verify_fun` can accept a `verify/4` function, which will receive:
+
+ * OtpCert, the other party's certificate [PKIX Certificates](`e:public_key:public_key_records.html#pkix-certificates`)
+ * DerCert, the other party's original [DER Encoded](`t:public_key:der_encoded/0`) certificate
+ * SslStatus, OTP's verification outcome, such as `valid` or a tuple `{bad_cert, unknown_ca}`
+ * Init will be `"any initial value"`
+
+The `verify/4` can use the DerCert for atypical workarounds such as
+handling decoding errors and directly verifying signatures.
+
+For more details see `{verify_fun, Verify}` [in common_option_cert](`t:ssl:common_option_cert/0`)
+
+
+> #### Note {: .info }
+> The legacy command line format for `verify_fun` cannot be used
+> in a `-ssl_dist_optfile` file as described below in
+> [Specifying TLS Options (Legacy)](#specifying-tls-options-legacy).
## Using TLS distribution over IPv6
diff -ruN otp-OTP-27.3.4/lib/ssl/doc/notes.md otp-OTP-27.3.4.1/lib/ssl/doc/notes.md
--- otp-OTP-27.3.4/lib/ssl/doc/notes.md 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/ssl/doc/notes.md 2025-06-16 11:27:55.000000000 +0300
@@ -21,6 +21,24 @@
This document describes the changes made to the SSL application.
+## SSL 11.2.12.1
+
+### Fixed Bugs and Malfunctions
+
+- hs_keylog callback properly handle alert in initial states, where encryption is not yet used. Also add keylog callback invocation for corner-case where server alert is encrypted with application secrets as client is already in connection state.
+
+ Own Id: OTP-19635 Aux Id: ERIERL-1235, [PR-9849]
+
+[PR-9849]: https://github.com/erlang/otp/pull/9849
+
+### Improvements and New Features
+
+- The documentation for SSL option `verify_fun` has been improved.
+
+ Own Id: OTP-19676 Aux Id: [PR-9691]
+
+[PR-9691]: https://github.com/erlang/otp/pull/9691
+
## SSL 11.2.12
### Improvements and New Features
diff -ruN otp-OTP-27.3.4/lib/ssl/src/ssl_gen_statem.erl otp-OTP-27.3.4.1/lib/ssl/src/ssl_gen_statem.erl
--- otp-OTP-27.3.4/lib/ssl/src/ssl_gen_statem.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/ssl/src/ssl_gen_statem.erl 2025-06-16 11:27:55.000000000 +0300
@@ -2246,9 +2246,9 @@
ok.
keylog_hs_alert(start, _) -> %% TLS 1.3: No secrets yet established
- [];
+ {[], undefined};
keylog_hs_alert(wait_sh, _) -> %% TLS 1.3: No secrets yet established
- [];
+ {[], undefined};
%% Server alert for certificate validation can happen when client is in connection state already.
keylog_hs_alert(connection, #state{static_env = #static_env{role = client},
connection_env =
diff -ruN otp-OTP-27.3.4/lib/ssl/src/tls_handshake_1_3.erl otp-OTP-27.3.4.1/lib/ssl/src/tls_handshake_1_3.erl
--- otp-OTP-27.3.4/lib/ssl/src/tls_handshake_1_3.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/ssl/src/tls_handshake_1_3.erl 2025-06-16 11:27:55.000000000 +0300
@@ -430,20 +430,17 @@
process_certificate(#certificate_1_3{
certificate_request_context = <<>>,
certificate_list = []},
- #state{ssl_options =
+ #state{static_env = #static_env{role = server},
+ ssl_options =
#{fail_if_no_peer_cert := false}} = State) ->
{ok, {State, wait_finished}};
process_certificate(#certificate_1_3{
certificate_request_context = <<>>,
certificate_list = []},
- #state{ssl_options =
+ #state{static_env = #static_env{role = server = Role},
+ ssl_options =
#{fail_if_no_peer_cert := true}} = State0) ->
- %% At this point the client believes that the connection is up and starts using
- %% its traffic secrets. In order to be able send an proper Alert to the client
- %% the server should also change its connection state and use the traffic
- %% secrets.
- State1 = calculate_traffic_secrets(State0),
- State = ssl_record:step_encryption_state(State1),
+ State = handle_alert_encryption_state(Role, State0),
{error, {?ALERT_REC(?FATAL, ?CERTIFICATE_REQUIRED, certificate_required), State}};
process_certificate(#certificate_1_3{certificate_list = CertEntries},
#state{ssl_options = SslOptions,
@@ -461,7 +458,7 @@
CertEntries, CertDbHandle, CertDbRef, SslOptions, CRLDbHandle, Role,
Host, StaplingState) of
#alert{} = Alert ->
- State = update_encryption_state(Role, State0),
+ State = handle_alert_encryption_state(Role, State0),
{error, {Alert, State}};
{PeerCert, PublicKeyInfo} ->
State = store_peer_cert(State0, PeerCert, PublicKeyInfo),
@@ -801,15 +798,33 @@
%% Sets correct encryption state when sending Alerts in shared states that use different secrets.
-%% - If client: use handshake secrets.
%% - If server: use traffic secrets as by this time the client's state machine
%% already stepped into the 'connection' state.
-update_encryption_state(server, State0) ->
+handle_alert_encryption_state(server, State0) ->
State1 = calculate_traffic_secrets(State0),
- ssl_record:step_encryption_state(State1);
-update_encryption_state(client, State) ->
+ #state{ssl_options = Options,
+ connection_states = ConnectionStates,
+ protocol_specific = PS} = State = ssl_record:step_encryption_state(State1),
+ KeylogFun = maps:get(keep_secrets, Options, undefined),
+ maybe_keylog(KeylogFun, PS, ConnectionStates),
+ State;
+%% - If client: use handshake secrets.
+handle_alert_encryption_state(client, State) ->
State.
+maybe_keylog({Keylog, Fun}, ProtocolSpecific, ConnectionStates) when Keylog == keylog_hs;
+ Keylog == keylog ->
+ N = maps:get(num_key_updates, ProtocolSpecific, 0),
+ #{security_parameters := #security_parameters{client_random = ClientRandom,
+ prf_algorithm = Prf,
+ application_traffic_secret = TrafficSecret}}
+ = ssl_record:current_connection_state(ConnectionStates, write),
+ TrafficKeyLog = ssl_logger:keylog_traffic_1_3(server, ClientRandom,
+ Prf, TrafficSecret, N),
+
+ ssl_logger:keylog(TrafficKeyLog, ClientRandom, Fun);
+maybe_keylog(_,_,_) ->
+ ok.
validate_certificate_chain(CertEntries, CertDbHandle, CertDbRef,
SslOptions, CRLDbHandle, Role, Host, StaplingState) ->
diff -ruN otp-OTP-27.3.4/lib/ssl/test/tls_1_3_version_SUITE.erl otp-OTP-27.3.4.1/lib/ssl/test/tls_1_3_version_SUITE.erl
--- otp-OTP-27.3.4/lib/ssl/test/tls_1_3_version_SUITE.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/ssl/test/tls_1_3_version_SUITE.erl 2025-06-16 11:27:55.000000000 +0300
@@ -575,9 +575,9 @@
{certfile, NewClientCertFile} | proplists:delete(certfile, ClientOpts0)],
ServerOpts = [{verify, verify_peer}, {fail_if_no_peer_cert, true}| ServerOpts0],
alert_passive(ServerOpts, ClientOpts, recv,
- ServerNode, Hostname),
+ ServerNode, Hostname, unknown_ca),
alert_passive(ServerOpts, ClientOpts, setopts,
- ServerNode, Hostname).
+ ServerNode, Hostname, unknown_ca).
tls13_client_tls11_server() ->
[{doc,"Test that a TLS 1.3 client gets old server alert from TLS 1.0 server."}].
@@ -609,7 +609,34 @@
Me ! {alert_info, AlertInfo}
end,
alert_passive([{keep_secrets, {keylog_hs, Fun}} | ServerOpts], ClientOpts, recv,
- ServerNode, Hostname),
+ ServerNode, Hostname, unknown_ca),
+
+ receive_server_keylog_for_client_cert_alert(),
+
+ alert_passive(ServerOpts, [{keep_secrets, {keylog_hs, Fun}} | ClientOpts], recv,
+ ServerNode, Hostname, unknown_ca),
+
+ receive_client_keylog_for_client_cert_alert(),
+
+ ClientNoCert = proplists:delete(keyfile, proplists:delete(certfile, ClientOpts0)),
+ alert_passive([{keep_secrets, {keylog_hs, Fun}} | ServerOpts], [{active, false} | ClientNoCert], recv,
+ ServerNode, Hostname, certificate_required),
+
+ receive_server_keylog_for_client_cert_alert().
+
+receive_server_keylog_for_client_cert_alert() ->
+ %% This alert will be decrypted with application secrets
+ %% as client is already in connection
+ receive
+ {alert_info, #{items := SKeyLog1}} ->
+ case SKeyLog1 of
+ ["SERVER_TRAFFIC_SECRET_0"++_] ->
+ ok;
+ S1Other ->
+ ct:fail({server_received, S1Other})
+ end
+ end,
+
receive
{alert_info, #{items := SKeyLog}} ->
case SKeyLog of
@@ -618,20 +645,18 @@
SOther ->
ct:fail({server_received, SOther})
end
- end,
+ end.
- alert_passive(ServerOpts, [{keep_secrets, {keylog_hs, Fun}} | ClientOpts], recv,
- ServerNode, Hostname),
+receive_client_keylog_for_client_cert_alert() ->
receive
{alert_info, #{items := CKeyLog}} ->
case CKeyLog of
- ["CLIENT_HANDSHAKE_TRAFFIC_SECRET"++_,_,_|_] ->
+ ["CLIENT_HANDSHAKE_TRAFFIC_SECRET"++_,_,_,_|_] ->
ok;
- COther ->
+ COther ->
ct:fail({client_received, COther})
end
end.
-
%%--------------------------------------------------------------------
%% Internal functions and callbacks -----------------------------------
%%--------------------------------------------------------------------
@@ -663,7 +688,7 @@
end.
alert_passive(ServerOpts, ClientOpts, Function,
- ServerNode, Hostname) ->
+ ServerNode, Hostname, AlertAtom) ->
Server = ssl_test_lib:start_server([{node, ServerNode}, {port, 0},
{from, self()},
{mfa, {ssl_test_lib, no_result, []}},
@@ -673,7 +698,7 @@
ct:sleep(500),
case Function of
recv ->
- {error, {tls_alert, {unknown_ca,_}}} = ssl:recv(Socket, 0);
+ {error, {tls_alert, {AlertAtom,_}}} = ssl:recv(Socket, 0);
setopts ->
{error, {tls_alert, {unknown_ca,_}}} = ssl:setopts(Socket, [{active, once}])
end.
diff -ruN otp-OTP-27.3.4/lib/ssl/vsn.mk otp-OTP-27.3.4.1/lib/ssl/vsn.mk
--- otp-OTP-27.3.4/lib/ssl/vsn.mk 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/ssl/vsn.mk 2025-06-16 11:27:55.000000000 +0300
@@ -1 +1 @@
-SSL_VSN = 11.2.12
+SSL_VSN = 11.2.12.1
diff -ruN otp-OTP-27.3.4/lib/stdlib/doc/notes.md otp-OTP-27.3.4.1/lib/stdlib/doc/notes.md
--- otp-OTP-27.3.4/lib/stdlib/doc/notes.md 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/stdlib/doc/notes.md 2025-06-16 11:27:55.000000000 +0300
@@ -21,6 +21,41 @@
This document describes the changes made to the STDLIB application.
+## STDLIB 6.2.2.1
+
+### Fixed Bugs and Malfunctions
+
+- The `save_module/1` command in the shell now saves both the locally defined records and the imported records using the `rr/1` command.
+
+ Own Id: OTP-19647 Aux Id: [GH-9816], [PR-9897]
+
+- It's now possible to write `lists:map(fun is_atom/1, [])` or `lists:map(fun my_func/1, [])`, in the shell, instead of `lists:map(fun erlang:is_atom/1, [])` or `lists:map(fun shell_default:my_func/1, [])`.
+
+ Own Id: OTP-19649 Aux Id: [GH-9771], [PR-9898]
+
+- Properly strip the leading `/` and drive letter from filepaths when zipping and unzipping archives.
+
+ Thanks to Wander Nauta for finding and responsibly disclosing this vulnerability to the Erlang/OTP project.
+
+ Own Id: OTP-19653 Aux Id: [CVE-2025-4748], [PR-9941]
+
+- Shell no longer crashes when requesting to autocomplete map keys containing non-atoms.
+
+ Own Id: OTP-19659 Aux Id: [PR-9896]
+
+- A remote shell can now exit by closing the input stream, without terminating the remote node.
+
+ Own Id: OTP-19667 Aux Id: [PR-9912]
+
+[GH-9816]: https://github.com/erlang/otp/issues/9816
+[PR-9897]: https://github.com/erlang/otp/pull/9897
+[GH-9771]: https://github.com/erlang/otp/issues/9771
+[PR-9898]: https://github.com/erlang/otp/pull/9898
+[CVE-2025-4748]: https://nvd.nist.gov/vuln/detail/2025-4748
+[PR-9941]: https://github.com/erlang/otp/pull/9941
+[PR-9896]: https://github.com/erlang/otp/pull/9896
+[PR-9912]: https://github.com/erlang/otp/pull/9912
+
## STDLIB 6.2.2
### Fixed Bugs and Malfunctions
diff -ruN otp-OTP-27.3.4/lib/stdlib/src/edlin_expand.erl otp-OTP-27.3.4.1/lib/stdlib/src/edlin_expand.erl
--- otp-OTP-27.3.4/lib/stdlib/src/edlin_expand.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/stdlib/src/edlin_expand.erl 2025-06-16 11:27:55.000000000 +0300
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2005-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2005-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -251,7 +251,7 @@
expand_map(Word, Bs, Binding, Keys) ->
case proplists:get_value(list_to_atom(Binding), Bs) of
Map when is_map(Map) ->
- K1 = sets:from_list(maps:keys(Map)),
+ K1 = sets:from_list([Key || Key <- maps:keys(Map), is_atom(Key)]),
K2 = sets:subtract(K1, sets:from_list([list_to_atom(K) || K <- Keys])),
match(Word, sets:to_list(K2), "=>");
_ -> {no, [], []}
diff -ruN otp-OTP-27.3.4/lib/stdlib/src/shell.erl otp-OTP-27.3.4.1/lib/stdlib/src/shell.erl
--- otp-OTP-27.3.4/lib/stdlib/src/shell.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/stdlib/src/shell.erl 2025-06-16 11:27:55.000000000 +0300
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 1996-2024. All Rights Reserved.
+%% Copyright Ericsson AB 1996-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -350,8 +350,15 @@
[N]),
server_loop(N0, Eval0, Bs0, RT, FT, Ds0, History0, Results0);
eof ->
- fwrite_severity(fatal, <<"Terminating erlang (~w)">>, [node()]),
- halt()
+ RemoteShell = node() =/= node(group_leader()),
+ case RemoteShell of
+ true ->
+ exit(Eval0, kill),
+ terminated;
+ false ->
+ fwrite_severity(fatal, <<"Terminating erlang (~w)">>, [node()]),
+ halt()
+ end
end.
get_command(Prompt, Eval, Bs, RT, FT, Ds) ->
@@ -365,7 +372,23 @@
io:scan_erl_exprs(group_leader(), Prompt, {1,1},
[text,{reserved_word_fun,ResWordFun}])
of
- {ok,Toks,_EndPos} ->
+ {ok,Toks0,_EndPos} ->
+ %% local 'fun' fixer
+ %% when we parse a 'fun' expression within a shell call or function definition
+ %% we need to add a local prefix (if the 'fun' expression did not have a module specified)
+ LocalFunFixer = fun F([{'fun',Anno}=A,{atom,_,Func}=B,{'/',_}=C,{integer,_,Arity}=D| Rest],Acc) ->
+ case erl_internal:bif(Func, Arity) of
+ true ->
+ F(Rest, [D,C,B,{':',A},{atom,Anno,'erlang'},A | Acc]);
+ false ->
+ F(Rest, [D,C,B,{':',A},{atom,Anno,'shell_default'},A | Acc])
+ end;
+ F([H|Rest], Acc) ->
+ F(Rest, [H | Acc]);
+ F([], Acc) ->
+ lists:reverse(Acc)
+ end,
+ Toks = LocalFunFixer(Toks0, []),
%% NOTE: we can handle function definitions, records and type declarations
%% but this cannot be handled by the function which only expects erl_parse:abstract_expressions()
%% for now just pattern match against those types and pass the string to shell local func.
@@ -1224,7 +1247,7 @@
%% In theory, you may want to be able to load a module in to local table
%% edit them, and then save it back to the file system.
%% You may also want to be able to save a test module.
-local_func(save_module, [{string,_,PathToFile}], Bs, _Shell, _RT, FT, _Lf, _Ef) ->
+local_func(save_module, [{string,_,PathToFile}], Bs, _Shell, RT, FT, _Lf, _Ef) ->
[_Path, FileName] = string:split("/"++PathToFile, "/", trailing),
[Module, _] = string:split(FileName, ".", leading),
Module1 = io_lib:fwrite("~tw",[list_to_atom(Module)]),
@@ -1232,8 +1255,8 @@
Output = (
"-module("++Module1++").\n\n" ++
"-export(["++lists:join(",",Exports)++"]).\n\n"++
- local_types(FT) ++
- local_records(FT) ++
+ local_types(FT) ++ "\n" ++
+ all_records(RT) ++
local_functions(FT)
),
Ret = case filelib:is_file(PathToFile) of
@@ -1452,12 +1475,13 @@
end || {F, A} <- Keys]).
%% Output local types
local_types(FT) ->
- lists:join($\n,
+ lists:join("\n\n",
[TypeDef||{{type_def, _},TypeDef} <- ets:tab2list(FT)]).
%% Output local records
local_records(FT) ->
- lists:join($\n,
- [RecDef||{{record_def, _},RecDef} <- ets:tab2list(FT)]).
+ [list_to_binary(RecDef)||{{record_def, _},RecDef} <- ets:tab2list(FT)].
+all_records(RT) ->
+ [list_to_binary(erl_pp:attribute(RecDef) ++ "\n")||{ _,RecDef} <- ets:tab2list(RT)].
write_and_compile_module(PathToFile, Output) ->
case file:write_file(PathToFile, unicode:characters_to_binary(Output)) of
ok -> c:c(PathToFile);
diff -ruN otp-OTP-27.3.4/lib/stdlib/src/stdlib.appup.src otp-OTP-27.3.4.1/lib/stdlib/src/stdlib.appup.src
--- otp-OTP-27.3.4/lib/stdlib/src/stdlib.appup.src 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/stdlib/src/stdlib.appup.src 2025-06-16 11:27:55.000000000 +0300
@@ -60,7 +60,8 @@
{<<"^6\\.1\\.2(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
{<<"^6\\.2$">>,[restart_new_emulator]},
{<<"^6\\.2\\.0(?:\\.[0-9]+)+$">>,[restart_new_emulator]},
- {<<"^6\\.2\\.1(?:\\.[0-9]+)*$">>,[restart_new_emulator]}],
+ {<<"^6\\.2\\.1(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
+ {<<"^6\\.2\\.2(?:\\.[0-9]+)*$">>,[restart_new_emulator]}],
[{<<"^4\\.0$">>,[restart_new_emulator]},
{<<"^4\\.0\\.0(?:\\.[0-9]+)+$">>,[restart_new_emulator]},
{<<"^4\\.0\\.1(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
@@ -93,4 +94,5 @@
{<<"^6\\.1\\.2(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
{<<"^6\\.2$">>,[restart_new_emulator]},
{<<"^6\\.2\\.0(?:\\.[0-9]+)+$">>,[restart_new_emulator]},
- {<<"^6\\.2\\.1(?:\\.[0-9]+)*$">>,[restart_new_emulator]}]}.
+ {<<"^6\\.2\\.1(?:\\.[0-9]+)*$">>,[restart_new_emulator]},
+ {<<"^6\\.2\\.2(?:\\.[0-9]+)*$">>,[restart_new_emulator]}]}.
diff -ruN otp-OTP-27.3.4/lib/stdlib/src/zip.erl otp-OTP-27.3.4.1/lib/stdlib/src/zip.erl
--- otp-OTP-27.3.4/lib/stdlib/src/zip.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/stdlib/src/zip.erl 2025-06-16 11:27:55.000000000 +0300
@@ -1237,12 +1237,12 @@
get_filename({Name, _, _}, Type) ->
get_filename(Name, Type);
get_filename(Name, regular) ->
- Name;
+ sanitize_filename(Name);
get_filename(Name, directory) ->
%% Ensure trailing slash
case lists:reverse(Name) of
- [$/ | _Rev] -> Name;
- Rev -> lists:reverse([$/ | Rev])
+ [$/ | _Rev] -> sanitize_filename(Name);
+ Rev -> sanitize_filename(lists:reverse([$/ | Rev]))
end.
add_cwd(_CWD, {_Name, _} = F) -> F;
@@ -2365,12 +2365,25 @@
get_filename_extra(FileNameLen, ExtraLen, B, GPFlag) ->
try
<<BFileName:FileNameLen/binary, BExtra:ExtraLen/binary>> = B,
- {binary_to_chars(BFileName, GPFlag), BExtra}
+ {sanitize_filename(binary_to_chars(BFileName, GPFlag)), BExtra}
catch
_:_ ->
throw(bad_file_header)
end.
+sanitize_filename(Filename) ->
+ case filename:pathtype(Filename) of
+ relative -> Filename;
+ _ ->
+ %% With absolute or volumerelative, we drop the prefix and rejoin
+ %% the path to create a relative path
+ Relative = filename:join(tl(filename:split(Filename))),
+ error_logger:format("Illegal absolute path: ~ts, converting to ~ts~n",
+ [Filename, Relative]),
+ relative = filename:pathtype(Relative),
+ Relative
+ end.
+
%% get compressed or stored data
get_z_data(?DEFLATED, In0, FileName, CompSize, Input, Output, OpO, Z) ->
ok = zlib:inflateInit(Z, -?MAX_WBITS),
diff -ruN otp-OTP-27.3.4/lib/stdlib/test/edlin_expand_SUITE.erl otp-OTP-27.3.4.1/lib/stdlib/test/edlin_expand_SUITE.erl
--- otp-OTP-27.3.4/lib/stdlib/test/edlin_expand_SUITE.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/stdlib/test/edlin_expand_SUITE.erl 2025-06-16 11:27:55.000000000 +0300
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2010-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2010-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -240,6 +240,9 @@
%% test that an already specified key does not get suggested again
{no, [], [{"a_key",_},{"c_key", _}]} = do_expand("MapBinding#{b_key=>1,"),
%% test that unicode works
+ {yes, "'??????'=>", []} = do_expand("UnicodeMap#{"),
+ %% test that non atoms are not suggested as completion
+ {no, "", []} = do_expand("NonAtomMap#{"),
ok.
function_parameter_completion(Config) ->
@@ -644,6 +647,8 @@
Bs = [
{'Binding', 0},
{'MapBinding', #{a_key=>0, b_key=>1, c_key=>2}},
+ {'UnicodeMap', #{'??????' => 0}},
+ {'NonAtomMap', #{{} => 1}},
{'RecordBinding', {some_record, 1, 2}},
{'TupleBinding', {0, 1, 2}},
{'S?ndag', 0},
diff -ruN otp-OTP-27.3.4/lib/stdlib/test/shell_SUITE.erl otp-OTP-27.3.4.1/lib/stdlib/test/shell_SUITE.erl
--- otp-OTP-27.3.4/lib/stdlib/test/shell_SUITE.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/stdlib/test/shell_SUITE.erl 2025-06-16 11:27:55.000000000 +0300
@@ -1,7 +1,7 @@
%%
%% %CopyrightBegin%
%%
-%% Copyright Ericsson AB 2004-2024. All Rights Reserved.
+%% Copyright Ericsson AB 2004-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
@@ -31,7 +31,7 @@
progex_lc/1, progex_funs/1,
otp_5990/1, otp_6166/1, otp_6554/1,
otp_7184/1, otp_7232/1, otp_8393/1, otp_10302/1, otp_13719/1,
- otp_14285/1, otp_14296/1, typed_records/1, types/1]).
+ otp_14285/1, otp_14296/1, typed_records/1, types/1, funs/1]).
-export([ start_restricted_from_shell/1,
start_restricted_on_command_line/1,restricted_local/1]).
@@ -351,6 +351,16 @@
"exception error: no function clause matching call to f/1" =
comm_err(<<"f(a).">>),
ok.
+funs(Config) when is_list(Config) ->
+ [[2,3,4]] = scan(<<"lists:map(fun ceil/1, [1.1, 2.1, 3.1]).">>),
+ rtnode:run(
+ [{putline, "add_one(X)-> X + 1."},
+ {expect, "ok"},
+ {putline, "lists:map(fun add_one/1, [1, 2, 3])."},
+ {expect, "[2,3,4]"}
+ ],[],"", ["[\"init:stop().\"]"]),
+ receive after 1000 -> ok end,
+ ok.
%% type definition support
types(Config) when is_list(Config) ->
@@ -689,12 +699,14 @@
<<"-spec my_func(X) -> X.\n"
"my_func(X) -> X.\n"
"lf().">>),
+ file:write_file("MY_MODULE_RECORD.hrl", "-record(grej,{b})."),
%% Save local definitions to a module
U = unicode:characters_to_binary("?"),
- "ok.\nok.\nok.\nok.\nok.\nok.\n{ok,'MY_MODULE'}.\n" = t({
+ "ok.\nok.\n[grej].\nok.\nok.\nok.\nok.\n{ok,'MY_MODULE'}.\n" = t({
<<"-type hej() :: integer().\n"
"-record(svej, {a :: hej()}).\n"
- "my_func(#svej{a=A}) -> A.\n"
+ "rr(\"MY_MODULE_RECORD.hrl\").\n"
+ "my_func(#svej{a=A}) -> #grej{b=A}.\n"
"-spec not_implemented(X) -> X.\n"
"-spec 'my_func",U/binary,"'(X) -> X.\n"
"'my_func",U/binary,"'(#svej{a=A}) -> A.\n"
@@ -702,14 +714,16 @@
%% Read back the newly created module
{ok,<<"-module('MY_MODULE').\n\n"
"-export([my_func/1,'my_func",240,159,152,138,"'/1]).\n\n"
- "-type hej() :: integer().\n"
- "-record(svej,{a :: hej()}).\n"
+ "-type hej() :: integer().\n\n"
+ "-record(grej,{b}).\n\n"
+ "-record(svej,{a :: hej()}).\n\n"
"my_func(#svej{a = A}) ->\n"
- " A.\n\n"
+ " #grej{b = A}.\n\n"
"-spec 'my_func",240,159,152,138,"'(X) -> X.\n"
"'my_func",240,159,152,138,"'(#svej{a = A}) ->\n"
" A.\n">>} = file:read_file("MY_MODULE.erl"),
file:delete("MY_MODULE.erl"),
+ file:delete("MY_MODULE_RECORD.erl"),
%% Forget one locally defined type
"ok.\nok.\nok.\n-type svej() :: integer().\n.\nok.\n" = t(
diff -ruN otp-OTP-27.3.4/lib/stdlib/test/zip_SUITE.erl otp-OTP-27.3.4.1/lib/stdlib/test/zip_SUITE.erl
--- otp-OTP-27.3.4/lib/stdlib/test/zip_SUITE.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/stdlib/test/zip_SUITE.erl 2025-06-16 11:27:55.000000000 +0300
@@ -25,7 +25,7 @@
-export([borderline/1, atomic/1,
bad_zip/1, unzip_from_binary/1, unzip_to_binary/1,
- zip_to_binary/1,
+ zip_to_binary/1, sanitize_filenames/1,
unzip_options/1, zip_options/1, list_dir_options/1, aliases/1,
zip_api/1, open_leak/1, unzip_jar/1,
unzip_traversal_exploit/1,
@@ -97,7 +97,7 @@
end.
zip_testcases() ->
- [mode, basic_timestamp, extended_timestamp, uid_gid].
+ [mode, basic_timestamp, extended_timestamp, uid_gid, sanitize_filenames].
zip64_testcases() ->
[unzip64_central_headers,
@@ -231,22 +231,27 @@
{ok, Archive} = zip:zip(Archive, [Name]),
ok = file:delete(Name),
+ RelName = filename:join(tl(filename:split(Name))),
+
%% Verify listing and extracting.
{ok, [#zip_comment{comment = []},
- #zip_file{name = Name,
+ #zip_file{name = RelName,
info = Info,
offset = 0,
comp_size = _}]} = zip:list_dir(Archive),
Size = Info#file_info.size,
- {ok, [Name]} = zip:extract(Archive, [verbose]),
+ TempRelName = filename:join(TempDir, RelName),
+ {ok, [TempRelName]} = zip:extract(Archive, [verbose, {cwd, TempDir}]),
- %% Verify contents of extracted file.
- {ok, Bin} = file:read_file(Name),
- true = match_byte_list(X0, binary_to_list(Bin)),
+ %% Verify that absolute file was not created
+ {error, enoent} = file:read_file(Name),
+ %% Verify that relative contents of extracted file.
+ {ok, Bin} = file:read_file(TempRelName),
+ true = match_byte_list(X0, binary_to_list(Bin)),
%% Verify that Unix zip can read it. (if we have a unix zip that is!)
- zipinfo_match(Archive, Name),
+ zipinfo_match(Archive, RelName),
ok.
@@ -1619,6 +1624,50 @@
ok.
+sanitize_filenames(Config) ->
+ RootDir = get_value(pdir, Config),
+ TempDir = filename:join(RootDir, "sanitize_filenames"),
+ ok = file:make_dir(TempDir),
+
+ %% Check that /tmp/absolute does not exist
+ {error, enoent} = file:read_file("/tmp/absolute"),
+
+ %% Create a zip archive /tmp/absolute in it
+ %% This file was created using the command below on Erlang/OTP 28.0
+ %% 1> rr(file), {ok, {_, Bin}} = zip:zip("absolute.zip", [{"/tmp/absolute",<<>>,#file_info{ type=regular, mtime={{2000,1,1},{0,0,0}}, size=0 }}], [memory]), rp(base64:encode(Bin)).
+ AbsZip = base64:decode(<<"UEsDBAoAAAAAAAAAISgAAAAAAAAAAAAAAAANAAkAL3RtcC9hYnNvbHV0ZVVUBQABcDVtOFBLAQI9AwoAAAAAAAAAISgAAAAAAAAAAAAAAAANAAkAAAAAAAAAAACkAQAAAAAvdG1wL2Fic29sdXRlVVQFAAFwNW04UEsFBgAAAAABAAEARAAAADQAAAAAAA==">>),
+ AbsArchive = filename:join(TempDir, "absolute.zip"),
+ ok = file:write_file(AbsArchive, AbsZip),
+
+ {ok, ["tmp/absolute"]} = unzip(Config, AbsArchive, [verbose, {cwd, TempDir}]),
+
+ zipinfo_match(AbsArchive, "/tmp/absolute"),
+
+ case un_z64(get_value(unzip, Config)) =/= unemzip of
+ true ->
+ {error, enoent} = file:read_file("/tmp/absolute"),
+ {ok, <<>>} = file:read_file(filename:join([TempDir, "tmp", "absolute"]));
+ false ->
+ ok
+ end,
+
+ RelArchive = filename:join(TempDir, "relative.zip"),
+ Relative = filename:join(TempDir, "relative"),
+ ok = file:write_file(Relative, <<>>),
+ ?assertMatch({ok, RelArchive},zip(Config, RelArchive, "", [Relative], [{cwd, TempDir}])),
+
+ SanitizedRelative = filename:join(tl(filename:split(Relative))),
+ case un_z64(get_value(unzip, Config)) =:= unemzip of
+ true ->
+ {ok, [SanitizedRelative]} = unzip(Config, RelArchive, [{cwd, TempDir}]);
+ false ->
+ ok
+ end,
+
+ zipinfo_match(RelArchive, SanitizedRelative),
+
+ ok.
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Generic zip interface
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
diff -ruN otp-OTP-27.3.4/lib/stdlib/vsn.mk otp-OTP-27.3.4.1/lib/stdlib/vsn.mk
--- otp-OTP-27.3.4/lib/stdlib/vsn.mk 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/stdlib/vsn.mk 2025-06-16 11:27:55.000000000 +0300
@@ -1 +1 @@
-STDLIB_VSN = 6.2.2
+STDLIB_VSN = 6.2.2.1
diff -ruN otp-OTP-27.3.4/lib/xmerl/doc/notes.md otp-OTP-27.3.4.1/lib/xmerl/doc/notes.md
--- otp-OTP-27.3.4/lib/xmerl/doc/notes.md 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/xmerl/doc/notes.md 2025-06-16 11:27:55.000000000 +0300
@@ -21,6 +21,18 @@
This document describes the changes made to the Xmerl application.
+## Xmerl 2.1.3.1
+
+### Fixed Bugs and Malfunctions
+
+- The type specs of `xmerl_scan:file/2` and `xmerl_scan:string/2`
+ has been updated to return `t:dynamic/0`. Due to hook functions
+ they can return any user defined term.
+
+ Own Id: OTP-19662 Aux Id: [PR-9905], ERIERL-1225
+
+[PR-9905]: https://github.com/erlang/otp/pull/9905
+
## Xmerl 2.1.3
### Improvements and New Features
diff -ruN otp-OTP-27.3.4/lib/xmerl/src/xmerl_scan.erl otp-OTP-27.3.4.1/lib/xmerl/src/xmerl_scan.erl
--- otp-OTP-27.3.4/lib/xmerl/src/xmerl_scan.erl 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/xmerl/src/xmerl_scan.erl 2025-06-16 11:27:55.000000000 +0300
@@ -340,7 +340,7 @@
-doc "Parse a file containing an XML document".
-spec file(Filename :: string(), option_list()) ->
- {document(), Rest} | {error, Reason} when
+ {dynamic(), Rest} | {error, Reason} when
Rest :: string(),
Reason :: term().
file(F, Options) ->
@@ -383,7 +383,7 @@
-doc "Parse a string containing an XML document".
-spec string(Text :: string(), option_list()) ->
- {document(), Rest} when
+ {dynamic(), Rest} when
Rest :: string().
string(Str, Options) ->
{Res, Tail, S=#xmerl_scanner{close_fun = Close}} =
diff -ruN otp-OTP-27.3.4/lib/xmerl/vsn.mk otp-OTP-27.3.4.1/lib/xmerl/vsn.mk
--- otp-OTP-27.3.4/lib/xmerl/vsn.mk 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/lib/xmerl/vsn.mk 2025-06-16 11:27:55.000000000 +0300
@@ -1 +1 @@
-XMERL_VSN = 2.1.3
+XMERL_VSN = 2.1.3.1
diff -ruN otp-OTP-27.3.4/make/doc.mk otp-OTP-27.3.4.1/make/doc.mk
--- otp-OTP-27.3.4/make/doc.mk 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/make/doc.mk 2025-06-16 11:27:55.000000000 +0300
@@ -1,7 +1,7 @@
#
# %CopyrightBegin%
#
-# Copyright Ericsson AB 1997-2024. All Rights Reserved.
+# Copyright Ericsson AB 1997-2025. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -64,7 +64,7 @@
endif
DOC_TARGETS?=$(DEFAULT_DOC_TARGETS)
-EX_DOC_WARNINGS_AS_ERRORS?=true
+EX_DOC_WARNINGS_AS_ERRORS?=default
docs: $(DOC_TARGETS)
diff -ruN otp-OTP-27.3.4/make/ex_doc_wrapper.in otp-OTP-27.3.4.1/make/ex_doc_wrapper.in
--- otp-OTP-27.3.4/make/ex_doc_wrapper.in 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/make/ex_doc_wrapper.in 2025-06-16 11:27:55.000000000 +0300
@@ -40,6 +40,16 @@
## Close fd 3 and 4
exec 3>&- 4>&-
+## If EX_DOC_WARNINGS_AS_ERRORS is not explicitly turned on
+## and any .app file is missing, we turn off warnings as errors
+if [ "${EX_DOC_WARNINGS_AS_ERRORS}" != "true" ]; then
+ for app in $ERL_TOP/lib/*/; do
+ if [ ! -f $app/ebin/*.app ]; then
+ EX_DOC_WARNINGS_AS_ERRORS=false
+ fi
+ done
+fi
+
if [ "${EX_DOC_WARNINGS_AS_ERRORS}" != "false" ]; then
if echo "${OUTPUT}" | grep "warning:" 1>/dev/null; then
echo "ex_doc emitted warnings"
diff -ruN otp-OTP-27.3.4/make/otp_version_tickets otp-OTP-27.3.4.1/make/otp_version_tickets
--- otp-OTP-27.3.4/make/otp_version_tickets 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/make/otp_version_tickets 2025-06-16 11:27:55.000000000 +0300
@@ -1,6 +1,14 @@
-OTP-19577
-OTP-19599
-OTP-19602
-OTP-19605
-OTP-19608
-OTP-19625
+OTP-19634
+OTP-19635
+OTP-19637
+OTP-19638
+OTP-19640
+OTP-19646
+OTP-19647
+OTP-19649
+OTP-19653
+OTP-19658
+OTP-19659
+OTP-19662
+OTP-19667
+OTP-19676
diff -ruN otp-OTP-27.3.4/OTP_VERSION otp-OTP-27.3.4.1/OTP_VERSION
--- otp-OTP-27.3.4/OTP_VERSION 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/OTP_VERSION 2025-06-16 11:27:55.000000000 +0300
@@ -1 +1 @@
-27.3.4
+27.3.4.1
diff -ruN otp-OTP-27.3.4/otp_versions.table otp-OTP-27.3.4.1/otp_versions.table
--- otp-OTP-27.3.4/otp_versions.table 2025-05-08 14:03:33.000000000 +0300
+++ otp-OTP-27.3.4.1/otp_versions.table 2025-06-16 11:27:55.000000000 +0300
@@ -1,3 +1,4 @@
+OTP-27.3.4.1 : asn1-5.3.4.1 eldap-1.2.14.1 kernel-10.2.7.1 ssh-5.2.11.1 ssl-11.2.12.1 stdlib-6.2.2.1 xmerl-2.1.3.1 # common_test-1.27.7 compiler-8.6.1 crypto-5.5.3 debugger-5.5 dialyzer-5.3.1 diameter-2.4.1 edoc-1.3.2 erl_interface-5.5.2 erts-15.2.7 et-1.7.1 eunit-2.9.1 ftp-1.2.3 inets-9.3.2 jinterface-1.14.1 megaco-4.7.2 mnesia-4.23.5 observer-2.17 odbc-2.15 os_mon-2.10.1 parsetools-2.6 public_key-1.17.1 reltool-1.0.1 runtime_tools-2.1.1 sasl-4.2.2 snmp-5.18.2 syntax_tools-3.2.2 tftp-1.2.2 tools-4.1.1 wx-2.4.3 :
OTP-27.3.4 : erts-15.2.7 kernel-10.2.7 ssh-5.2.11 xmerl-2.1.3 # asn1-5.3.4 common_test-1.27.7 compiler-8.6.1 crypto-5.5.3 debugger-5.5 dialyzer-5.3.1 diameter-2.4.1 edoc-1.3.2 eldap-1.2.14 erl_interface-5.5.2 et-1.7.1 eunit-2.9.1 ftp-1.2.3 inets-9.3.2 jinterface-1.14.1 megaco-4.7.2 mnesia-4.23.5 observer-2.17 odbc-2.15 os_mon-2.10.1 parsetools-2.6 public_key-1.17.1 reltool-1.0.1 runtime_tools-2.1.1 sasl-4.2.2 snmp-5.18.2 ssl-11.2.12 stdlib-6.2.2 syntax_tools-3.2.2 tftp-1.2.2 tools-4.1.1 wx-2.4.3 :
OTP-27.3.3 : erts-15.2.6 kernel-10.2.6 megaco-4.7.2 ssh-5.2.10 ssl-11.2.12 # asn1-5.3.4 common_test-1.27.7 compiler-8.6.1 crypto-5.5.3 debugger-5.5 dialyzer-5.3.1 diameter-2.4.1 edoc-1.3.2 eldap-1.2.14 erl_interface-5.5.2 et-1.7.1 eunit-2.9.1 ftp-1.2.3 inets-9.3.2 jinterface-1.14.1 mnesia-4.23.5 observer-2.17 odbc-2.15 os_mon-2.10.1 parsetools-2.6 public_key-1.17.1 reltool-1.0.1 runtime_tools-2.1.1 sasl-4.2.2 snmp-5.18.2 stdlib-6.2.2 syntax_tools-3.2.2 tftp-1.2.2 tools-4.1.1 wx-2.4.3 xmerl-2.1.2 :
OTP-27.3.2 : asn1-5.3.4 compiler-8.6.1 erts-15.2.5 kernel-10.2.5 megaco-4.7.1 snmp-5.18.2 ssl-11.2.11 xmerl-2.1.2 # common_test-1.27.7 crypto-5.5.3 debugger-5.5 dialyzer-5.3.1 diameter-2.4.1 edoc-1.3.2 eldap-1.2.14 erl_interface-5.5.2 et-1.7.1 eunit-2.9.1 ftp-1.2.3 inets-9.3.2 jinterface-1.14.1 mnesia-4.23.5 observer-2.17 odbc-2.15 os_mon-2.10.1 parsetools-2.6 public_key-1.17.1 reltool-1.0.1 runtime_tools-2.1.1 sasl-4.2.2 ssh-5.2.9 stdlib-6.2.2 syntax_tools-3.2.2 tftp-1.2.2 tools-4.1.1 wx-2.4.3 :
-------------- next part --------------
From: Lukas Backstrom <lukas at erlang.org>
Date: Tue, 27 May 2025 21:50:01 +0200
Subject: [PATCH] stdlib: Properly sanatize filenames when (un)zipping
According to the Zip APPNOTE filenames "MUST NOT contain a drive or
device letter, or a leading slash.". So we strip those when zipping
and unzipping.
Origin: https://github.com/erlang/otp/commit/ee67d46285394db95133709cef74b0c462d665aa
Bug-Debian: https://bugs.debian.org/1107939
Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2025-4748
--- a/lib/stdlib/src/zip.erl
+++ b/lib/stdlib/src/zip.erl
@@ -826,12 +826,12 @@
get_filename({Name, _, _}, Type) ->
get_filename(Name, Type);
get_filename(Name, regular) ->
- Name;
+ sanitize_filename(Name);
get_filename(Name, directory) ->
%% Ensure trailing slash
case lists:reverse(Name) of
- [$/ | _Rev] -> Name;
- Rev -> lists:reverse([$/ | Rev])
+ [$/ | _Rev] -> sanitize_filename(Name);
+ Rev -> sanitize_filename(lists:reverse([$/ | Rev]))
end.
add_cwd(_CWD, {_Name, _} = F) -> F;
@@ -1531,12 +1531,25 @@
get_file_name_extra(FileNameLen, ExtraLen, B, GPFlag) ->
try
<<BFileName:FileNameLen/binary, BExtra:ExtraLen/binary>> = B,
- {binary_to_chars(BFileName, GPFlag), BExtra}
+ {sanitize_filename(binary_to_chars(BFileName, GPFlag)), BExtra}
catch
_:_ ->
throw(bad_file_header)
end.
+sanitize_filename(Filename) ->
+ case filename:pathtype(Filename) of
+ relative -> Filename;
+ _ ->
+ %% With absolute or volumerelative, we drop the prefix and rejoin
+ %% the path to create a relative path
+ Relative = filename:join(tl(filename:split(Filename))),
+ error_logger:format("Illegal absolute path: ~ts, converting to ~ts~n",
+ [Filename, Relative]),
+ relative = filename:pathtype(Relative),
+ Relative
+ end.
+
%% get compressed or stored data
get_z_data(?DEFLATED, In0, FileName, CompSize, Input, Output, OpO, Z) ->
ok = zlib:inflateInit(Z, -?MAX_WBITS),
--- a/lib/stdlib/test/zip_SUITE.erl
+++ b/lib/stdlib/test/zip_SUITE.erl
@@ -22,7 +22,7 @@
-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1,
init_per_group/2,end_per_group/2, borderline/1, atomic/1,
bad_zip/1, unzip_from_binary/1, unzip_to_binary/1,
- zip_to_binary/1,
+ zip_to_binary/1, sanitize_filenames/1,
unzip_options/1, zip_options/1, list_dir_options/1, aliases/1,
openzip_api/1, zip_api/1, open_leak/1, unzip_jar/1,
unzip_traversal_exploit/1,
@@ -40,7 +40,8 @@
unzip_to_binary, zip_to_binary, unzip_options,
zip_options, list_dir_options, aliases, openzip_api,
zip_api, open_leak, unzip_jar, compress_control, foldl,
- unzip_traversal_exploit,fd_leak,unicode,test_zip_dir].
+ unzip_traversal_exploit,fd_leak,unicode,test_zip_dir,
+ sanitize_filenames].
groups() ->
[].
@@ -90,22 +91,27 @@
{ok, Archive} = zip:zip(Archive, [Name]),
ok = file:delete(Name),
+ RelName = filename:join(tl(filename:split(Name))),
+
%% Verify listing and extracting.
{ok, [#zip_comment{comment = []},
- #zip_file{name = Name,
+ #zip_file{name = RelName,
info = Info,
offset = 0,
comp_size = _}]} = zip:list_dir(Archive),
Size = Info#file_info.size,
- {ok, [Name]} = zip:extract(Archive, [verbose]),
+ TempRelName = filename:join(TempDir, RelName),
+ {ok, [TempRelName]} = zip:extract(Archive, [verbose, {cwd, TempDir}]),
- %% Verify contents of extracted file.
- {ok, Bin} = file:read_file(Name),
- true = match_byte_list(X0, binary_to_list(Bin)),
+ %% Verify that absolute file was not created
+ {error, enoent} = file:read_file(Name),
+ %% Verify that relative contents of extracted file.
+ {ok, Bin} = file:read_file(TempRelName),
+ true = match_byte_list(X0, binary_to_list(Bin)),
%% Verify that Unix zip can read it. (if we have a unix zip that is!)
- zipinfo_match(Archive, Name),
+ zipinfo_match(Archive, RelName),
ok.
@@ -1052,3 +1058,21 @@
end
end)().
+sanitize_filenames(Config) ->
+ RootDir = proplists:get_value(priv_dir, Config),
+ TempDir = filename:join(RootDir, "borderline"),
+ ok = file:make_dir(TempDir),
+
+ %% Create a zip archive /tmp/absolute in it
+ %% This file was created using the command below on Erlang/OTP 28.0
+ %% 1> rr(file), {ok, {_, Bin}} = zip:zip("absolute.zip", [{"/tmp/absolute",<<>>,#file_info{ type=regular, mtime={{1970,1,1},{0,0,0}}, size=0 }}], [memory]), rp(base64:encode(Bin)).
+ AbsZip = base64:decode(<<"UEsDBBQAAAAAAAAAIewAAAAAAAAAAAAAAAANAAAAL3RtcC9hYnNvbHV0ZVBLAQIUAxQAAAAAAAAAIewAAAAAAAAAAAAAAAANAAAAAAAAAAAAAACkAQAAAAAvdG1wL2Fic29sdXRlUEsFBgAAAAABAAEAOwAAACsAAAAAAA==">>),
+ Archive = filename:join(TempDir, "absolute.zip"),
+ ok = file:write_file(Archive, AbsZip),
+
+ TmpAbs = filename:join([TempDir, "tmp", "absolute"]),
+ {ok, [TmpAbs]} = zip:unzip(Archive, [verbose, {cwd, TempDir}]),
+ {error, enoent} = file:read_file("/tmp/absolute"),
+ {ok, <<>>} = file:read_file(TmpAbs),
+
+ ok.
\ No newline at end of file
More information about the Pkg-erlang-devel
mailing list