Package: release.debian.org Severity: normal X-Debbugs-Cc: erl...@packages.debian.org Control: affects -1 + src:erlang User: release.debian....@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
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 :
From: Lukas Backstrom <lu...@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