Your message dated Thu, 10 Jul 2025 12:30:19 +0000
with message-id <e1uzqpr-00egwc...@respighi.debian.org>
and subject line unblock erlang
has caused the Debian Bug report #1108338,
regarding preapproval for unblock: erlang/1:27.3.4.1+dfsg-1 or 
erlang/1:27.3.4+dfsg-1 with a patch
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact ow...@bugs.debian.org
immediately.)


-- 
1108338: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1108338
Debian Bug Tracking System
Contact ow...@bugs.debian.org with problems
--- Begin Message ---
Package: release.debian.org
Severity: normal
X-Debbugs-Cc: erl...@packages.debian.org
Control: affects -1 + src:erlang
User: release.debian....@packages.debian.org
Usertags: unblock

Hi!

I'd like to upload a fix for CVE-2025-4748 (insufficient path
sanitizing when extracting from ZIP apchives, see [1],[2] for details).
Upstream fix this bug in 27.3.4.1, but the changes include fixes for
several other bugs (27.3.4.1 is strictly a bugfix release). I'd like
to have these fixes in trixie as well.

So what would be better, to upload minimal changes which fix only
CVE-2025-4748, or the full 27.3.4.1?

I'm attaching both the full diff for 27.3.4.1 and separately the excerpt
from it concerning CVE-2025-4748.

[1] https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1107939
[2] https://security-tracker.debian.org/tracker/CVE-2025-4748

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

--- a/lib/stdlib/src/zip.erl
+++ b/lib/stdlib/src/zip.erl
@@ -826,12 +826,12 @@
 get_filename({Name, _, _}, Type) ->
     get_filename(Name, Type);
 get_filename(Name, regular) ->
-    Name;
+    sanitize_filename(Name);
 get_filename(Name, directory) ->
     %% Ensure trailing slash
     case lists:reverse(Name) of
-       [$/ | _Rev] -> Name;
-       Rev         -> lists:reverse([$/ | Rev])
+       [$/ | _Rev] -> sanitize_filename(Name);
+       Rev         -> sanitize_filename(lists:reverse([$/ | Rev]))
     end.
 
 add_cwd(_CWD, {_Name, _} = F) -> F;
@@ -1531,12 +1531,25 @@
 get_file_name_extra(FileNameLen, ExtraLen, B, GPFlag) ->
     try
         <<BFileName:FileNameLen/binary, BExtra:ExtraLen/binary>> = B,
-        {binary_to_chars(BFileName, GPFlag), BExtra}
+        {sanitize_filename(binary_to_chars(BFileName, GPFlag)), BExtra}
     catch
         _:_ ->
             throw(bad_file_header)
     end.
 
+sanitize_filename(Filename) ->
+    case filename:pathtype(Filename) of
+        relative -> Filename;
+        _ ->
+            %% With absolute or volumerelative, we drop the prefix and rejoin
+            %% the path to create a relative path
+            Relative = filename:join(tl(filename:split(Filename))),
+            error_logger:format("Illegal absolute path: ~ts, converting to 
~ts~n",
+                                [Filename, Relative]),
+            relative = filename:pathtype(Relative),
+            Relative
+    end.
+
 %% get compressed or stored data
 get_z_data(?DEFLATED, In0, FileName, CompSize, Input, Output, OpO, Z) ->
     ok = zlib:inflateInit(Z, -?MAX_WBITS),
--- a/lib/stdlib/test/zip_SUITE.erl
+++ b/lib/stdlib/test/zip_SUITE.erl
@@ -22,7 +22,7 @@
 -export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, 
         init_per_group/2,end_per_group/2, borderline/1, atomic/1,
          bad_zip/1, unzip_from_binary/1, unzip_to_binary/1,
-         zip_to_binary/1,
+         zip_to_binary/1, sanitize_filenames/1,
          unzip_options/1, zip_options/1, list_dir_options/1, aliases/1,
          openzip_api/1, zip_api/1, open_leak/1, unzip_jar/1,
         unzip_traversal_exploit/1,
@@ -40,7 +40,8 @@
      unzip_to_binary, zip_to_binary, unzip_options,
      zip_options, list_dir_options, aliases, openzip_api,
      zip_api, open_leak, unzip_jar, compress_control, foldl,
-     unzip_traversal_exploit,fd_leak,unicode,test_zip_dir].
+     unzip_traversal_exploit,fd_leak,unicode,test_zip_dir,
+     sanitize_filenames].
 
 groups() -> 
     [].
@@ -90,22 +91,27 @@
     {ok, Archive} = zip:zip(Archive, [Name]),
     ok = file:delete(Name),
 
+    RelName = filename:join(tl(filename:split(Name))),
+
     %% Verify listing and extracting.
     {ok, [#zip_comment{comment = []},
-          #zip_file{name = Name,
+          #zip_file{name = RelName,
                     info = Info,
                     offset = 0,
                     comp_size = _}]} = zip:list_dir(Archive),
     Size = Info#file_info.size,
-    {ok, [Name]} = zip:extract(Archive, [verbose]),
+    TempRelName = filename:join(TempDir, RelName),
+    {ok, [TempRelName]} = zip:extract(Archive, [verbose, {cwd, TempDir}]),
 
-    %% Verify contents of extracted file.
-    {ok, Bin} = file:read_file(Name),
-    true = match_byte_list(X0, binary_to_list(Bin)),
+    %% Verify that absolute file was not created
+    {error, enoent} = file:read_file(Name),
 
+    %% Verify that relative contents of extracted file.
+    {ok, Bin} = file:read_file(TempRelName),
+    true = match_byte_list(X0, binary_to_list(Bin)),
 
     %% Verify that Unix zip can read it. (if we have a unix zip that is!)
-    zipinfo_match(Archive, Name),
+    zipinfo_match(Archive, RelName),
 
     ok.
 
@@ -1052,3 +1058,21 @@
              end
      end)().
     
+sanitize_filenames(Config) ->
+    RootDir = proplists:get_value(priv_dir, Config),
+    TempDir = filename:join(RootDir, "borderline"),
+    ok = file:make_dir(TempDir),
+
+    %% Create a zip archive /tmp/absolute in it
+    %%   This file was created using the command below on Erlang/OTP 28.0
+    %%   1> rr(file), {ok, {_, Bin}} = zip:zip("absolute.zip", 
[{"/tmp/absolute",<<>>,#file_info{ type=regular, mtime={{1970,1,1},{0,0,0}}, 
size=0 }}], [memory]), rp(base64:encode(Bin)).
+    AbsZip = 
base64:decode(<<"UEsDBBQAAAAAAAAAIewAAAAAAAAAAAAAAAANAAAAL3RtcC9hYnNvbHV0ZVBLAQIUAxQAAAAAAAAAIewAAAAAAAAAAAAAAAANAAAAAAAAAAAAAACkAQAAAAAvdG1wL2Fic29sdXRlUEsFBgAAAAABAAEAOwAAACsAAAAAAA==">>),
+    Archive = filename:join(TempDir, "absolute.zip"),
+    ok = file:write_file(Archive, AbsZip),
+
+    TmpAbs = filename:join([TempDir, "tmp", "absolute"]),
+    {ok, [TmpAbs]} = zip:unzip(Archive, [verbose, {cwd, TempDir}]),
+    {error, enoent} = file:read_file("/tmp/absolute"),
+    {ok, <<>>} = file:read_file(TmpAbs),
+
+    ok.
\ No newline at end of file

--- End Message ---
--- Begin Message ---
Unblocked.

--- End Message ---

Reply via email to