[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