Version in base suite: 27.3.4.1+dfsg-1+deb13u2 Base version: erlang_27.3.4.1+dfsg-1+deb13u2 Target version: erlang_27.3.4.1+dfsg-1+deb13u3 Base file: /srv/ftp-master.debian.org/ftp/pool/main/e/erlang/erlang_27.3.4.1+dfsg-1+deb13u2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/e/erlang/erlang_27.3.4.1+dfsg-1+deb13u3.dsc changelog | 79 ++ patches/CVE-2026-28808.patch | 235 ++++++ patches/CVE-2026-28810.patch | 1209 ++++++++++++++++++++++++++++++++++ patches/CVE-2026-32144-1.patch | 195 +++++ patches/CVE-2026-32144-2.patch | 387 +++++++++++ patches/CVE-2026-32147.patch | 1434 +++++++++++++++++++++++++++++++++++++++++ patches/CVE-2026-42789.patch | 187 +++++ patches/CVE-2026-42790.patch | 476 +++++++++++++ patches/CVE-2026-42791.patch | 125 +++ patches/CVE-2026-42792-1.patch | 32 patches/CVE-2026-42792-2.patch | 59 + patches/CVE-2026-47078.patch | 160 ++++ patches/CVE-2026-48855.patch | 254 +++++++ patches/CVE-2026-48856.patch | 129 +++ patches/CVE-2026-48858.patch | 198 +++++ patches/CVE-2026-48860.patch | 29 patches/CVE-2026-49759.patch | 217 ++++++ patches/CVE-2026-49760.patch | 135 +++ patches/CVE-2026-53422.patch | 449 ++++++++++++ patches/CVE-2026-54886.patch | 93 ++ patches/CVE-2026-54887.patch | 29 patches/CVE-2026-54890.patch | 52 + patches/CVE-2026-54891.patch | 434 ++++++++++++ patches/CVE-2026-55737.patch | 47 + patches/CVE-2026-55950.patch | 38 + patches/CVE-2026-55952.patch | 35 + patches/CVE-2026-55953.patch | 383 ++++++++++ patches/CVE-2026-58227.patch | 378 ++++++++++ patches/CVE-2026-59250.patch | 52 + patches/CVE-2026-59251.patch | 581 ++++++++++++++++ patches/series | 30 patches/ssh-test-macros.patch | 58 + 32 files changed, 8199 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpukdt25av/erlang_27.3.4.1+dfsg-1+deb13u2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpukdt25av/erlang_27.3.4.1+dfsg-1+deb13u3.dsc: no acceptable signature found diff -Nru erlang-27.3.4.1+dfsg/debian/changelog erlang-27.3.4.1+dfsg/debian/changelog --- erlang-27.3.4.1+dfsg/debian/changelog 2026-04-04 13:45:31.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/changelog 2026-08-22 19:38:30.000000000 +0000 @@ -1,3 +1,82 @@ +erlang (1:27.3.4.1+dfsg-1+deb13u3) trixie-security; urgency=medium + + [ Aron Xu ] + * Add a series of patches by upstream, which fix a set of vulnerabilities: + - Fix CVE-2026-48855: Exposure of Sensitive Information to an Unauthorized + Actor vulnerability in Erlang OTP ssh application (ssh_sftpd module). + - Fix CVE-2026-48856: Sensitive Data Exposure vulnerability in Erlang OTP + inets application (httpc_response module). + - Fix CVE-2026-48858: Server-Side Request Forgery (SSRF) vulnerability in + Erlang/OTP ftp application (ftp_internal module). + - Fix CVE-2026-48859: Observable Timing Discrepancy vulnerability in + Erlang/OTP ssh application (ssh_auth, ssh_options modules). + - Fix CVE-2026-48860: Reliance on IP Address for Authentication + vulnerability in Erlang/OTP ssl application (inet_tls_dist module). + - Fix CVE-2026-49759: Stack-based Buffer Overflow vulnerability in Erlang + OTP erts (inet_drv). + - Fix CVE-2026-49760: Stack-based Buffer Overflow vulnerability in Erlang + OTP (erl_interface). + Closes: #1139727, #1139823. + - Fix CVE-2026-53422: Observable Response Discrepancy vulnerability in + Erlang OTP ssh application (ssh_sftpd module). + - Fix CVE-2026-54886: Loop with Unreachable Exit Condition ('Infinite + Loop') vulnerability in Erlang OTP ssh application (ssh_sftpd module). + - Fix CVE-2026-54887: Use of Default Cryptographic Key vulnerability in + Erlang/OTP ssl application (DTLS server) + - Fix CVE-2026-54891: Improper Enforcement of Message Integrity During + Transmission in a Communication Channel vulnerability in Erlang/OTP ssl + application (tls_gen_connection module). + - Fix CVE-2026-55950: Time-of-check Time-of-use (TOCTOU) race condition + vulnerability in Erlang/OTP ssl application (dtls_packet_demux module). + - Fix CVE-2026-55952: The Erlang/OTP ssl application does not validate + that the PSK identity list and binder list carried in a TLS 1.3 + ClientHello pre-shared key extension have equal length before passing + them to the session ticket handler. + Closes: #1141414. + - Fix CVE-2026-42792: Improper Handling of Exceptional Conditions + vulnerability in Erlang/OTP epmd daemon. + - Fix CVE-2026-47078: Relative Path Traversal vulnerability in Erlang/OTP + stdlib (zip module). + - Fix CVE-2026-54890: Integer Underflow (Wrap or Wraparound) vulnerability + in Erlang/OTP erts. + - Fix CVE-2026-55737: Signed to Unsigned Conversion Error and + Out-of-bounds Write vulnerability in Erlang/OTP erts. + - Fix CVE-2026-55953: The Erlang/OTP ssl TLS and DTLS client does not + verify that the cipher suite selected by the server in ServerHello + was among the suites offered by the client in ClientHello. + - Fix CVE-2026-58227: The Erlang/OTP ssl application does not detect + cycles when reconstructing an incomplete peer certificate chain during + a TLS or DTLS handshake. + - Fix CVE-2026-59250: Buffer overflow in the Erlang/OTP megaco flex + scanner C driver allows a remote unauthenticated attacker to corrupt + the driver's memory. + - Fix CVE-2026-59251: Allocation of resources without limits in Erlang/OTP + public_key certificate path validation allows a remote unauthenticated + attacker to cause denial of service. + Closes: #1142985. + - Fix CVE-2026-28808: Incorrect Authorization vulnerability in Erlang/OTP + (inets modules) allows unauthenticated access to CGI scripts. + - Fix CVE-2026-28810: Generation of Predictable Numbers or Identifiers + vulnerability in Erlang/OTP kernel (inet_res, inet_db modules) allows + DNS Cache Poisoning. + - Fix CVE-2026-32144: Improper Certificate Validation vulnerability in + Erlang/OTP public_key (pubkey_ocsp module) allows OCSP + designated-responder authorization bypass via missing signature + verification. + - Fix CVE-2026-32147: Vulnerability in the SFTP server where file + attributes could be modified outside the configured root directory. + - Fix CVE-2026-42789: Improper Following of a Certificate's Chain of Trust + vulnerability in Erlang/OTP public_key application allows a non-CA + certificate to be accepted as an intermediate issuer. + - Fix CVE-2026-42790: Improper Certificate Validation vulnerability in + Erlang/OTP public_key application allows a DNS nameConstraints bypass + via subject CommonName fallback in TLS hostname verification. + - Fix CVE-2026-42791: Improper Certificate Validation vulnerability in + Erlang/OTP public_key application allows forged OCSP responses signed + with an expired responder certificate to be accepted as valid. + + -- Sergei Golovan Sat, 22 Aug 2026 22:38:30 +0300 + erlang (1:27.3.4.1+dfsg-1+deb13u2) trixie; urgency=medium [ Lucas Kanashiro ] diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-28808.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-28808.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-28808.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-28808.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,235 @@ +From 2d05c8ab7895befdfc8f9684a06b1f728365a52b Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Tue, 7 Apr 2026 09:36:39 +0200 +Subject: [PATCH] Merge branch 'whaileee/inets/httpd/cgi-auth-skip/OTP-20068' + into maint-27 + +* whaileee/inets/httpd/cgi-auth-skip/OTP-20068: + inets: Check script_alias when using mod_auth + +(cherry picked from commit 2f3cd39a634148472fdf89e31b16bd6a7459633c) +--- + lib/inets/src/http_server/httpd.erl | 8 +++ + lib/inets/src/http_server/mod_alias.erl | 10 +++- + lib/inets/test/httpd_SUITE.erl | 80 ++++++++++++++++++++++++- + 3 files changed, 94 insertions(+), 4 deletions(-) + +diff --git a/lib/inets/src/http_server/httpd.erl b/lib/inets/src/http_server/httpd.erl +index b467329afe..c7b8b49637 100644 +--- a/lib/inets/src/http_server/httpd.erl ++++ b/lib/inets/src/http_server/httpd.erl +@@ -435,6 +435,14 @@ property list. + Access to http://your.server.org/cgi-bin/foo would cause the server to run the + script /web/cgi-bin/foo. + ++ > #### Note {: .info } ++ > ++ > When using `script_alias` with directory-based authentication ++ > (see [`directory`](`m:httpd#prop_dri`)), ensure that authentication ++ > rules reference the actual filesystem path (RealName), not the URL path (Alias). ++ > The server correctly resolves script_alias paths for authentication checks. ++ > ++ + - [](){: #prop_script_re_write } **`{script_re_write, {Re, Replacement}}`** + `Re = string()` and `Replacement = string()`. Have the same behavior as + property `re_write`, except that they also mark the target directory as +diff --git a/lib/inets/src/http_server/mod_alias.erl b/lib/inets/src/http_server/mod_alias.erl +index b387e9e5a9..18125b78d1 100644 +--- a/lib/inets/src/http_server/mod_alias.erl ++++ b/lib/inets/src/http_server/mod_alias.erl +@@ -309,6 +309,13 @@ store({re_write, {Re, Replacement}} = Conf, _) + end; + store({re_write, _} = Conf, _) -> + {error, {wrong_type, Conf}}; ++ ++% When `script_alias` is used in conjunction with `m:mod_auth` for directory-based ++% access control, authentication rules are evaluated against the actual filesystem ++% path where scripts reside, not the aliased URL path. This ensures that CGI scripts ++% mapped outside the document root are properly protected by directory authentication ++% directives. ++ + store({script_alias, {Fake, Real}}, _) + when is_list(Fake), is_list(Real) -> + {ok, {script_alias,{"^"++Fake,Real}}}; +@@ -339,7 +346,8 @@ is_directory_index_list(_) -> + %% --------------------------------------------------------------------- + + which_alias(ConfigDB) -> +- httpd_util:multi_lookup(ConfigDB, alias). ++ httpd_util:multi_lookup(ConfigDB, alias) ++ ++ httpd_util:multi_lookup(ConfigDB, script_alias). + + which_document_root(ConfigDB) -> + Root = httpd_util:lookup(ConfigDB, document_root, ""), +diff --git a/lib/inets/test/httpd_SUITE.erl b/lib/inets/test/httpd_SUITE.erl +index 0ee9fcccd6..0e7d5d08c7 100644 +--- a/lib/inets/test/httpd_SUITE.erl ++++ b/lib/inets/test/httpd_SUITE.erl +@@ -80,6 +80,7 @@ all() -> + {group, http_logging}, + {group, http_post}, + {group, http_rel_path_script_alias}, ++ {group, http_script_alias_auth}, + {group, http_not_sup}, + {group, https_alert}, + {group, https_not_sup}, +@@ -144,6 +145,7 @@ groups() -> + {http_1_0, [], [cgi_bin_env, {group, http_1_0_parallel} | load()]}, + {http_1_0_parallel, [parallel], [host, cgi, trace] ++ http_head() ++ http_get()}, + {http_rel_path_script_alias, [], [cgi]}, ++ {http_script_alias_auth, [], [script_alias_auth_bypass]}, + {esi, [], [erl_script_timeout_default, + erl_script_timeout_option, + erl_script_timeout_proplist, +@@ -277,6 +279,9 @@ init_per_group(http_logging, Config) -> + init_per_group(http_rel_path_script_alias = Group, Config) -> + ok = start_apps(Group), + init_httpd(Group, [{type, ip_comm},{http_version, "HTTP/1.1"}| Config]); ++init_per_group(http_script_alias_auth = Group, Config) -> ++ ok = start_apps(Group), ++ init_httpd(Group, [{type, ip_comm},{http_version, "HTTP/1.1"}| Config]); + init_per_group(not_sup, Config) -> + [{http_version, "HTTP/1.1"} | Config]; + init_per_group(Group, Config) when Group == esi -> +@@ -298,6 +303,7 @@ end_per_group(Group, _Config) when Group == http_basic; + Group == http_mime_type; + Group == http_mime_and_default_type; + Group == http_mime_types; ++ Group == http_script_alias_auth; + Group == esi + -> + inets:stop(); +@@ -1141,6 +1147,34 @@ cgi(Config) when is_list(Config) -> + [{statuscode, 200}, + {no_header, "cache-control"}]). + %%------------------------------------------------------------------------- ++script_alias_auth_bypass() -> ++ [{doc, "Test that mod_auth correctly protects script_alias directories " ++ "outside DocumentRoot (CVE-2026-28808)"}]. ++script_alias_auth_bypass(Config) when is_list(Config) -> ++ Version = proplists:get_value(http_version, Config), ++ Host = proplists:get_value(host, Config), ++ Script = ++ case os:type() of ++ {win32, _} -> "printenv.bat"; ++ _ -> "printenv.sh" ++ end, ++ %% Unauthenticated request must be rejected with 401 ++ ok = http_status("GET /http_script_alias_auth/" ++ Script ++ " ", Config, ++ [{statuscode, 401}, ++ {header, "WWW-Authenticate"}]), ++ %% Authenticated request must succeed ++ ok = auth_status( ++ auth_request("/http_script_alias_auth/" ++ Script, "one", "onePassword", ++ Version, Host), ++ Config, ++ [{statuscode, 200}]), ++ %% Wrong password must be rejected ++ ok = auth_status( ++ auth_request("/http_script_alias_auth/" ++ Script, "one", "WrongPassword", ++ Version, Host), ++ Config, ++ [{statuscode, 401}]). ++%%------------------------------------------------------------------------- + cgi_chunked_encoding_test() -> + [{doc, "Test chunked encoding together with mod_cgi "}]. + cgi_chunked_encoding_test(Config) when is_list(Config) -> +@@ -2127,6 +2161,7 @@ do_max_clients(Config) -> + + setup_server_dirs(ServerRoot, DocRoot, DataDir) -> + CgiDir = filename:join(ServerRoot, "cgi-bin"), ++ ExtCgiDir = filename:join(ServerRoot, "ext-cgi-bin"), + AuthDir = filename:join(ServerRoot, "auth"), + PicsDir = filename:join(ServerRoot, "icons"), + ConfigDir = filename:join(ServerRoot, "config"), +@@ -2134,6 +2169,7 @@ setup_server_dirs(ServerRoot, DocRoot, DataDir) -> + ok = file:make_dir(ServerRoot), + ok = file:make_dir(DocRoot), + ok = file:make_dir(CgiDir), ++ ok = file:make_dir(ExtCgiDir), + ok = file:make_dir(AuthDir), + ok = file:make_dir(PicsDir), + ok = file:make_dir(ConfigDir), +@@ -2147,6 +2183,7 @@ setup_server_dirs(ServerRoot, DocRoot, DataDir) -> + inets_test_lib:copy_dirs(DocSrc, DocRoot), + inets_test_lib:copy_dirs(AuthSrc, AuthDir), + inets_test_lib:copy_dirs(CgiSrc, CgiDir), ++ inets_test_lib:copy_dirs(CgiSrc, ExtCgiDir), + inets_test_lib:copy_dirs(PicsSrc, PicsDir), + inets_test_lib:copy_dirs(ConfigSrc, ConfigDir), + +@@ -2165,7 +2202,13 @@ setup_server_dirs(ServerRoot, DocRoot, DataDir) -> + EnvCGI = filename:join([ServerRoot, "cgi-bin", "printenv.sh"]), + {ok, FileInfo1} = file:read_file_info(EnvCGI), + ok = file:write_file_info(EnvCGI, +- FileInfo1#file_info{mode = 8#00755}). ++ FileInfo1#file_info{mode = 8#00755}), ++ ++ %% Set permissions for ext-cgi-bin scripts (outside DocumentRoot) ++ ExtEnvCGI = filename:join([ServerRoot, "ext-cgi-bin", "printenv.sh"]), ++ {ok, FileInfo2} = file:read_file_info(ExtEnvCGI), ++ ok = file:write_file_info(ExtEnvCGI, ++ FileInfo2#file_info{mode = 8#00755}). + + setup_tmp_dir(PrivDir) -> + TmpDir = filename:join(PrivDir, "tmp"), +@@ -2204,6 +2247,7 @@ start_apps(Group) when Group == http_basic; + Group == http_mime_and_default_type; + Group == http_mime_types; + Group == http_rel_path_script_alias; ++ Group == http_script_alias_auth; + Group == http_not_sup; + Group == http_mime_types; + Group == esi -> +@@ -2323,6 +2367,20 @@ server_config(http_erl_script_alias_all, Config) -> + server_config(http_rel_path_script_alias, Config) -> + ServerRoot = proplists:get_value(server_root, Config), + config_template(Config, ServerRoot, "./cgi-bin/", [httpd_example, io]); ++server_config(http_script_alias_auth, Config) -> ++ ServerRoot = proplists:get_value(server_root, Config), ++ %% CGI dir is outside DocumentRoot (sibling under ServerRoot) ++ ExtCgiDir = filename:join(ServerRoot, "ext-cgi-bin") ++ "/", ++ [{modules, [mod_alias, mod_auth, ?MODULE, mod_get, mod_head]}, ++ {logger, [{error, httpd_test}]}, ++ {script_alias, {"/http_script_alias_auth/", ExtCgiDir}}, ++ {directory, {filename:join(ServerRoot, "ext-cgi-bin"), ++ [{auth_type, plain}, ++ {auth_name, "Protected CGI"}, ++ {auth_user_file, filename:join(ServerRoot, "auth/passwd")}, ++ {auth_group_file, filename:join(ServerRoot, "auth/group")}, ++ {require_user, ["one", "Aladdin"]}]}} ++ ] ++ server_config(http, Config); + server_config(https, Config) -> + SSLConf = proplists:get_value(ssl_conf, Config), + ServerConf = proplists:get_value(server_config, SSLConf), +@@ -2410,9 +2468,25 @@ do(ModData) -> + ok; + _ -> + {already_sent, Status, _Size} = proplists:get_value(response, ModData#mod.data), +- propagate_test ! {status, Status} ++ propagate_test ! {status, Status} + end, +- {proceed, ModData#mod.data}. ++ case ModData#mod.request_uri of ++ "/http_script_alias_auth/" ++ _ -> ++ case proplists:get_value(status, ModData#mod.data) of ++ {_StatusCode, _PhraseArgs, _Reason} -> ++ {proceed, ModData#mod.data}; ++ undefined -> ++ case proplists:get_value(response, ModData#mod.data) of ++ undefined -> ++ Body = "script_alias_auth_bypass test ok", ++ {proceed, [{response, {200, Body}} | ModData#mod.data]}; ++ _Response -> ++ {proceed, ModData#mod.data} ++ end ++ end; ++ _ -> ++ {proceed, ModData#mod.data} ++ end. + + not_sup_conf() -> + [{modules, [mod_get]}]. +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-28810.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-28810.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-28810.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-28810.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,1209 @@ +From 53c4794b95c8334064a8e647a983545e62af897d Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Tue, 7 Apr 2026 09:36:33 +0200 +Subject: [PATCH] Merge branch 'raimo/kernel/inet_res-id-27/OTP-20037' into + maint-27 + +* raimo/kernel/inet_res-id-27/OTP-20037: + Update after feedback + Revert "Avoid duplicate DNS transaction IDs" + Avoid duplicate DNS transaction IDs + Randomize `inet_res` transaction ID and source port number + +# Conflicts: +# lib/kernel/src/inet.erl + +(cherry picked from commit f01d4e32e439ebef774dee0efe7a4c9a0bd36dca) +--- + lib/kernel/src/gen_udp.erl | 2 +- + lib/kernel/src/inet.erl | 50 ++++- + lib/kernel/src/inet6_sctp.erl | 9 +- + lib/kernel/src/inet6_udp.erl | 8 +- + lib/kernel/src/inet_db.erl | 73 +++++- + lib/kernel/src/inet_dns.erl | 177 ++++++++++----- + lib/kernel/src/inet_res.erl | 346 ++++++++++++++++++----------- + lib/kernel/src/inet_sctp.erl | 8 +- + lib/kernel/src/inet_udp.erl | 9 +- + lib/kernel/test/inet_res_SUITE.erl | 4 +- + 10 files changed, 462 insertions(+), 224 deletions(-) + +diff --git a/lib/kernel/src/gen_udp.erl b/lib/kernel/src/gen_udp.erl +index 97fce0603d..e66c856f9f 100644 +--- a/lib/kernel/src/gen_udp.erl ++++ b/lib/kernel/src/gen_udp.erl +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 1997-2024. All Rights Reserved. ++%% Copyright Ericsson AB 1997-2026. 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. +diff --git a/lib/kernel/src/inet.erl b/lib/kernel/src/inet.erl +index 3c12ee14b3..83899262e3 100644 +--- a/lib/kernel/src/inet.erl ++++ b/lib/kernel/src/inet.erl +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 1997-2025. All Rights Reserved. ++%% Copyright Ericsson AB 1997-2026. 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. +@@ -3927,13 +3927,14 @@ gethostbyaddr_tm_native(Addr, Timer, Opts) -> + {'ok', port()} | {'error', posix()}. + + open(Fd, BAddr, BPort, Opts, Protocol, Family, Type, Module) +- when is_integer(Fd), 0 =< Fd -> ++ when is_integer(Fd), 0 =< Fd, is_integer(BPort) -> + open_fd(Fd, BAddr, BPort, Opts, Protocol, Family, Type, Module); +-open(Fd_or_OpenOpts, BAddr, BPort, Opts, Protocol, Family, Type, Module) -> ++open(Fd_or_OpenOpts, BAddr, BPort, Opts, Protocol, Family, Type, Module) ++ when is_integer(BPort) -> + open_opts( + Fd_or_OpenOpts, + if +- BAddr =:= undefined, BPort =/= 0 -> ++ BAddr =:= undefined, BPort > 0 -> + translate_ip(any, Family); + true -> + BAddr +@@ -3982,14 +3983,15 @@ open(Fd_or_OpenOpts, BAddr, BPort, Opts, Protocol, Family, Type, Module) -> + {'ok', port()} | {'error', posix()}. + + open_bind(Fd, BAddr, BPort, Opts, Protocol, Family, Type, Module) +- when is_integer(Fd), 0 =< Fd -> ++ when is_integer(Fd), 0 =< Fd, is_integer(BPort) -> + %% ?DBG([{fd, Fd}, + %% {baddr, BAddr}, {bport, BPort}, + %% {opts, Opts}, {proto, Protocol}, {fam, Family}, + %% {type, Type}, {mod, Module}]), + open_fd(Fd, BAddr, BPort, Opts, Protocol, Family, Type, Module); + open_bind( +- Fd_or_OpenOpts, BAddr, BPort, Opts, Protocol, Family, Type, Module) -> ++ Fd_or_OpenOpts, BAddr, BPort, Opts, Protocol, Family, Type, Module) ++ when is_integer(BPort) -> + %% ?DBG([{fd_or_openopts, Fd_or_OpenOpts}, + %% {baddr, BAddr}, {bport, BPort}, + %% {opts, Opts}, {proto, Protocol}, {fam, Family}, +@@ -4075,11 +4077,39 @@ open_setopts(S, BAddr, BPort, Opts, Module) -> + + + +-bind(S, Addr, Port) when is_list(Addr) -> ++bind(S, Addr, Port) when is_list(Addr), is_integer(Port) -> + bindx(S, Addr, Port); +-bind(S, Addr, Port) -> +- %% ?DBG([{s, S}, {addr, Addr}, {port, Port}]), +- prim_inet:bind(S, Addr, Port). ++bind(S, Addr, -1) -> ++ bind_random(S, Addr); ++bind(S, Addr, Port) when is_integer(Port) -> ++ do_bind(S, Addr, Port). ++ ++do_bind(S, Addr, Port) -> ++ Result = prim_inet:bind(S, Addr, Port), ++ %% ?DBG([{s, S}, {addr, Addr}, {port, Port}, Result]), ++ Result. ++ ++bind_random(S, Addr) -> ++ Cnt = 3, ++ bind_random(S, Addr, Cnt). ++%% ++bind_random(S, Addr, 0 = _Cnt) -> ++ Port = 0, ++ do_bind(S, Addr, Port); ++bind_random(S, Addr, Cnt) when is_integer(Cnt), 0 < Cnt -> ++ Port = inet_db:res_option(random_port), ++ bind_random(S, Addr, Cnt, Port). ++%% ++bind_random(S, Addr, _Cnt, 0 = Port) -> ++ do_bind(S, Addr, Port); ++bind_random(S, Addr, Cnt, Port) when ?port(Port) -> ++ case prim_inet:bind(S, Addr, Port) of ++ {ok, _} = OK -> ++ %% ?DBG([{s, S}, {addr, Addr}, {port, Port}, OK]), ++ OK; ++ {error, _} -> ++ bind_random(S, Addr, Cnt - 1) ++ end. + + bindx(S, [Addr], Port0) -> + {IP, Port} = set_bindx_port(Addr, Port0), +diff --git a/lib/kernel/src/inet6_sctp.erl b/lib/kernel/src/inet6_sctp.erl +index ef75ec193a..7437026553 100644 +--- a/lib/kernel/src/inet6_sctp.erl ++++ b/lib/kernel/src/inet6_sctp.erl +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2007-2024. All Rights Reserved. ++%% Copyright Ericsson AB 2007-2026. 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. +@@ -55,11 +55,12 @@ open(Opts) -> + ifaddr = Addr, + port = Port, + type = Type, +- opts = SOs}} -> ++ opts = SOs}} ++ when ?port(Port) -> + inet:open_bind( + Fd, Addr, Port, SOs, ?PROTO, ?FAMILY, Type, ?MODULE); +- Error -> +- Error ++ {ok, _} -> {error, badarg}; ++ Error -> Error + end. + + close(S) -> +diff --git a/lib/kernel/src/inet6_udp.erl b/lib/kernel/src/inet6_udp.erl +index 5ac60b0951..7c148dd1b8 100644 +--- a/lib/kernel/src/inet6_udp.erl ++++ b/lib/kernel/src/inet6_udp.erl +@@ -1,8 +1,8 @@ + %% + %% %CopyrightBegin% +-%% +-%% Copyright Ericsson AB 1997-2024. All Rights Reserved. +-%% ++%% ++%% Copyright Ericsson AB 1997-2026. 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. + %% You may obtain a copy of the License at +@@ -64,6 +64,8 @@ open(Port, Opts) -> + port = BPort, + opts = SockOpts}} + when is_map(BAddr); % sockaddr_in() ++ BPort =:= -1, ?ip6(BAddr); ++ BPort =:= -1, BAddr =:= undefined; + ?port(BPort), ?ip6(BAddr); + ?port(BPort), BAddr =:= undefined -> + %% ?DBG(['udp-options', +diff --git a/lib/kernel/src/inet_db.erl b/lib/kernel/src/inet_db.erl +index 00cfa42272..f61f68b5ab 100644 +--- a/lib/kernel/src/inet_db.erl ++++ b/lib/kernel/src/inet_db.erl +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 1997-2025. All Rights Reserved. ++%% Copyright Ericsson AB 1997-2026. 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. +@@ -314,7 +314,7 @@ valid_lookup() -> [dns, file, yp, nis, nisplus, native]. + %% Reconstruct an inetrc structure from inet_db + get_rc() -> + get_rc([hosts, domain, nameservers, search, alt_nameservers, +- timeout, retry, servfail_retry_timeout, inet6, usevc, ++ timeout, retry, servfail_retry_timeout, inet6, usevc, random, + edns, udp_payload_size, dnssec_ok, resolv_conf, hosts_file, + socks5_server, socks5_port, socks5_methods, socks5_noproxy, + udp, sctp, tcp, host, cache_size, cache_refresh, lookup], []). +@@ -356,6 +356,10 @@ get_rc([K | Ks], Ls) -> + res_usevc, + false, + Ks, Ls); ++ random -> get_rc(random, ++ res_random, ++ true, ++ Ks, Ls); + edns -> get_rc(edns, + res_edns, + false, +@@ -447,18 +451,15 @@ get_rc_hosts([], Ls) -> + get_rc_hosts([{{_Fam, IP}, Names} | Hosts], Ls) -> + get_rc_hosts(Hosts, [{host, IP, Names} | Ls]). + ++%% Some odd features stuffed into this API ++%% ++res_option(next_id) -> ++ generate_next_id(); ++res_option(random_port) -> ++ generate_random_port(); + %% + %% Resolver options + %% +-res_option(next_id) -> +- Cnt = ets:update_counter(inet_db, res_id, 1), +- case Cnt band 16#ffff of +- 0 -> +- _ = ets:update_counter(inet_db, res_id, -Cnt), +- 0; +- Id -> +- Id +- end; + res_option(Option) -> + case res_optname(Option) of + undefined -> +@@ -488,6 +489,7 @@ res_optname(servfail_retry_timeout) -> res_servfail_retry_timeout; + res_optname(timeout) -> res_timeout; + res_optname(inet6) -> res_inet6; + res_optname(usevc) -> res_usevc; ++res_optname(random) -> res_random; + res_optname(edns) -> res_edns; + res_optname(udp_payload_size) -> res_udp_payload_size; + res_optname(dnssec_ok) -> res_dnssec_ok; +@@ -523,6 +525,7 @@ res_check_option(servfail_retry_timeout, T) when is_integer(T), T >= 0 -> true; + res_check_option(timeout, T) when is_integer(T), T > 0 -> true; + res_check_option(inet6, Bool) when is_boolean(Bool) -> true; + res_check_option(usevc, Bool) when is_boolean(Bool) -> true; ++res_check_option(random, Bool) when is_boolean(Bool) -> true; + res_check_option(edns, V) when V =:= false; V =:= 0 -> true; + res_check_option(udp_payload_size, S) when is_integer(S), S >= 512 -> true; + res_check_option(dnssec_ok, D) when is_boolean(D) -> true; +@@ -880,13 +883,13 @@ take_socket_type(MRef) -> + %% res_search [Domain] - list of domains for short names + %% res_domain Domain - local domain for short names + %% res_recurse Bool - recursive query +-%% res_usevc Bool - use tcp only + %% res_id Integer - NS query identifier + %% res_retry Integer - Retry count for UDP query + %% res_servfail_retry_timeout Integer - Timeout to next query after a failure + %% res_timeout Integer - UDP query timeout before retry + %% res_inet6 Bool - address family inet6 for gethostbyname/1 + %% res_usevc Bool - use Virtual Circuit (TCP) ++%% res_random Bool - use random res_id and port number + %% res_edns false|Integer - false or EDNS version + %% res_udp_payload_size Integer - size for EDNS, both query and reply + %% res_dnssec_ok Bool - the DO bit in RFC6891 & RFC3225 +@@ -960,6 +963,7 @@ reset_db(Db) -> + {res_lookup, []}, + {res_recurse, true}, + {res_usevc, false}, ++ {res_random, true}, + {res_id, 0}, + {res_retry, ?RES_RETRY}, + {res_servfail_retry_timeout, ?RES_SERVFAIL_RETRY_TO}, +@@ -1649,6 +1653,7 @@ is_res_set(servfail_retry_timeout) -> true; + is_res_set(retry) -> true; + is_res_set(inet6) -> true; + is_res_set(usevc) -> true; ++is_res_set(random) -> true; + is_res_set(edns) -> true; + is_res_set(udp_payload_size) -> true; + is_res_set(dnssec_ok) -> true; +@@ -2077,3 +2082,47 @@ handle_take_socket_type(Db, MRef) -> + [] -> % Already demonitor'ed + error + end. ++ ++%%---------------------------------------------------------------------- ++%% Random DNS Transaction ID and origin port number ++%%---------------------------------------------------------------------- ++ ++generate_next_id() -> ++ case ets:lookup_element(inet_db, res_random, 2) of ++ true -> ++ case crypto_rand_range(1 bsl 16) of ++ Id when is_integer(Id), 0 =< Id, Id < 1 bsl 16 -> Id; ++ undefined -> ++ generate_next_id_legacy() ++ end; ++ false -> ++ generate_next_id_legacy() ++ end. ++ ++generate_next_id_legacy() -> ++ ets:update_counter(inet_db, res_id, {2, 1, 16#ffff, 0}). ++ ++generate_random_port() -> ++ Min = 1024, ++ Range = (1 bsl 16) - Min, ++ case crypto_rand_range(Range) of ++ V when is_integer(V), 0 =< V, V < Range -> Min + V; ++ undefined -> 0 ++ end. ++ ++%% We use `crypto:rand_uniform/2` here, which is the simplest to use, ++%% but it is deprecated, for outdated reasons. In really old, now obsolete, ++%% libcrypto versions the function was not cryptographically strong, ++%% but since OpenSSL 1.1.0 that is no longer the case. ++-compile({nowarn_deprecated_function, {crypto,rand_uniform,2}}). ++ ++crypto_rand_range(Range) when is_integer(Range), 0 < Range -> ++ %% This is how crypto itself checks if it is loaded ++ case application:get_env(crypto, fips_mode) of ++ undefined -> undefined; ++ {ok, Fips} when is_boolean(Fips) -> ++ try crypto:rand_uniform(0, Range) of ++ N when is_integer(N), 0 =< N, N < Range -> N ++ catch error : low_entropy -> undefined ++ end ++ end. +diff --git a/lib/kernel/src/inet_dns.erl b/lib/kernel/src/inet_dns.erl +index 05a8682de6..0a298c702f 100644 +--- a/lib/kernel/src/inet_dns.erl ++++ b/lib/kernel/src/inet_dns.erl +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 1997-2024. All Rights Reserved. ++%% Copyright Ericsson AB 1997-2026. 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. +@@ -29,6 +29,7 @@ + %% RFC 2181: Clarifications to the DNS Specification + %% RFC 2782: A DNS RR for specifying the location of services (DNS SRV) + %% RFC 2915: The Naming Authority Pointer (NAPTR) DNS Resource Rec ++%% RFC 5452: Measures for Making DNS More Resilient against Forged Answers + %% RFC 5936: DNS Zone Transfer Protocol (AXFR) + %% RFC 6488: DNS Certification Authority Authorization (CAA) Resource Record + %% RFC 6762: Multicast DNS +@@ -36,7 +37,8 @@ + %% RFC 7553: The Uniform Resource Identifier (URI) DNS Resource Record + %% RFC 8945: Secret Key Transaction Authentication for DNS (TSIG) + +--export([decode/1, decode/2, encode/1, encode/2]). ++-export([decode/1, decode/2, decode_reply/3, ++ update_id/2, encode/1, encode/2]). + -export([decode_algname/1, encode_algname/1]). + + -import(lists, [reverse/1]). +@@ -160,52 +162,106 @@ decode(Buffer, Mdns) when is_binary(Buffer), is_boolean(Mdns) -> + {error,Reason} + end. + +-do_decode(<>=Buffer, Mdns) -> ++decode_reply(Buffer, #dns_rec{} = Q, Mdns) ++ when is_binary(Buffer), is_boolean(Mdns) -> ++ try do_decode_reply(Buffer, Q, Mdns) of ++ DnsReq -> {ok, DnsReq} ++ catch ++ Reason -> ++ {error, Reason} ++ end. ++ ++do_decode( ++ <> = Buffer, Mdns) -> ++ %% + {AnBuf,QdList,QdTC} = decode_query_section(QdBuf,QdCount,Buffer,Mdns), ++ H_TC = decode_boolean(TC), ++ QdTC andalso not H_TC ++ andalso throw(?DECODE_ERROR), ++ DnsHdr = ++ #dns_header{ ++ id = Id, ++ qr = decode_boolean(QR), ++ opcode = decode_opcode(Opcode), ++ aa = decode_boolean(AA), ++ tc = H_TC, ++ rd = decode_boolean(RD), ++ ra = decode_boolean(RA), ++ pr = decode_boolean(PR), ++ rcode = Rcode}, ++ do_decode( ++ Buffer, DnsHdr, QdList, AnBuf, AnCount, NsCount, ArCount, {Opcode,Mdns}). ++ ++do_decode_reply( ++ <> = Buffer, ++ #dns_rec{ header = Q_H, qdlist = [Q_RR] }, ++ Mdns) -> ++ Id =:= Q_H#dns_header.id orelse throw(badid), ++ do_decode_reply(Buffer, Q_H, Q_RR, Id, Mdns). ++ ++do_decode_reply( ++ <<_:16, ++ QR:1,Opcode:4,AA:1,TC:1,RD:1, ++ RA:1,PR:1,_:2,Rcode:4, ++ QdCount:16,AnCount:16,NsCount:16,ArCount:16, ++ QdBuf/binary>> = Buffer, ++ Q_H, Q_RR, Id, Mdns) -> ++ %% ++ (H_QR = decode_boolean(QR)) ++ orelse throw(unknown), ++ (H_Opcode = decode_opcode(Opcode)) =:= Q_H#dns_header.opcode ++ orelse throw(unknown), ++ (H_RD = decode_boolean(RD)) andalso not Q_H#dns_header.rd ++ andalso throw(unknown), ++ %% ++ QdCount == 1 ++ orelse throw(noquery), ++ {AnBuf, [RR], QdTC} = decode_query_section(QdBuf, QdCount, Buffer, Mdns), ++ RR#dns_query.class =:= Q_RR#dns_query.class andalso ++ RR#dns_query.type =:= Q_RR#dns_query.type andalso ++ inet_db:eq_domains(RR#dns_query.domain, Q_RR#dns_query.domain) ++ orelse throw(noquery), ++ H_TC = decode_boolean(TC), ++ QdTC andalso not H_TC ++ andalso throw(?DECODE_ERROR), ++ DnsHdr = ++ #dns_header{ ++ id = Id, ++ qr = H_QR, ++ opcode = H_Opcode, ++ aa = decode_boolean(AA), ++ tc = H_TC, ++ rd = H_RD, ++ ra = decode_boolean(RA), ++ pr = decode_boolean(PR), ++ rcode = Rcode}, ++ do_decode( ++ Buffer, DnsHdr, [RR], AnBuf, AnCount, NsCount, ArCount, {Opcode,Mdns}); ++do_decode_reply(<<_/binary>>, _Q_H, _Q_RR, _Id, _Mdns) -> ++ throw(unknown). ++ ++do_decode(Buffer, DnsHdr, QdList, AnBuf, AnCount, NsCount, ArCount, Opts) -> + {NsBuf,AnList,AnTC} = +- decode_rr_section(AnBuf,AnCount,Buffer,{Opcode,Mdns}), ++ decode_rr_section(AnBuf, AnCount, Buffer, Opts), + {ArBuf,NsList,NsTC} = +- decode_rr_section(NsBuf,NsCount,Buffer,{Opcode,Mdns}), ++ decode_rr_section(NsBuf, NsCount, Buffer, Opts), + {Rest,ArList,ArTC} = +- decode_rr_section(ArBuf,ArCount,Buffer,{Opcode,Mdns}), +- ?MATCH_ELSE_DECODE_ERROR( +- Rest, +- <<>>, +- begin +- HdrTC = decode_boolean(TC), +- DnsHdr = +- #dns_header{id=Id, +- qr=decode_boolean(QR), +- opcode=decode_opcode(Opcode), +- aa=decode_boolean(AA), +- tc=HdrTC, +- rd=decode_boolean(RD), +- ra=decode_boolean(RA), +- pr=decode_boolean(PR), +- rcode=Rcode}, +- ?MATCH_ELSE_DECODE_ERROR( +- %% Header marked as truncated, or no section +- %% marked as truncated. +- %% The converse; a section marked as truncated, +- %% but not the header - is a parse error. +- %% +- HdrTC or (not (QdTC or AnTC or NsTC or ArTC)), +- true, +- begin +- #dns_rec{header=DnsHdr, +- qdlist=QdList, +- anlist=AnList, +- nslist=NsList, +- arlist=ArList} +- end) +- end); +-do_decode(_, _) -> +- %% DNS message does not even match header +- throw(?DECODE_ERROR). ++ decode_rr_section(ArBuf, ArCount, Buffer, Opts), ++ Rest =:= <<>> ++ orelse throw(?DECODE_ERROR), ++ ((AnTC orelse NsTC orelse ArTC) =:= DnsHdr#dns_header.tc) ++ orelse throw(?DECODE_ERROR), ++ #dns_rec{ ++ header = DnsHdr, ++ qdlist = QdList, ++ anlist = AnList, ++ nslist = NsList, ++ arlist = ArList}. ++ + + decode_query_section(Bin, N, Buffer, Mdns) -> + decode_query_section(Bin, N, Buffer, Mdns, []). +@@ -302,27 +358,34 @@ decode_rr_section(Bin, N, Buffer, {Opcode,Mdns} = Opts, RRs) -> + %% Encode a user query + %% + ++%% Update the ID field ++update_id(<<_:16, EncMsg/binary>>, Id) -> ++ [<>, EncMsg]; ++update_id([<<_:16>> | EncMsg], Id) -> ++ [<> | EncMsg]. ++ ++ + encode(Q) -> encode(Q, true). % Backwards compatible + %% + encode( + #dns_rec{ +- header = Header, +- qdlist = QdList, anlist = AnList, nslist = NsList, arlist = ArList }, +- Mdns) +- when is_boolean(Mdns) -> +- B0 = +- encode_header( +- Header, +- length(QdList), length(AnList), length(NsList), length(ArList)), +- Opcode = Header#dns_header.opcode, ++ header = Header = #dns_header{ opcode = Opcode }, ++ qdlist = QdList, ++ anlist = AnList, ++ nslist = NsList, ++ arlist = ArList }, ++ Mdns) when is_boolean(Mdns) -> ++ B0 = encode_header( ++ Header, ++ length(QdList), length(AnList), length(NsList), length(ArList)), ++ Opts = {Opcode,Mdns}, + C0 = gb_trees:empty(), + {B1,C1} = encode_query_section(B0, Mdns, C0, QdList), +- {B2,C2} = encode_res_section(B1, {Opcode,Mdns}, C1, AnList), +- {B3,C3} = encode_res_section(B2, {Opcode,Mdns}, C2, NsList), +- {B,_} = encode_res_section(B3, {Opcode,Mdns}, C3, ArList), ++ {B2,C2} = encode_res_section(B1, Opts, C1, AnList), ++ {B3,C3} = encode_res_section(B2, Opts, C2, NsList), ++ {B,_} = encode_res_section(B3, Opts, C3, ArList), + B. + +- + %% RFC 1035: 4.1.1. Header section format + %% + encode_header(#dns_header{id=Id}=H, QdCount, AnCount, NsCount, ArCount) -> +diff --git a/lib/kernel/src/inet_res.erl b/lib/kernel/src/inet_res.erl +index 0c47b1fb3c..35166643f7 100644 +--- a/lib/kernel/src/inet_res.erl ++++ b/lib/kernel/src/inet_res.erl +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 1997-2024. All Rights Reserved. ++%% Copyright Ericsson AB 1997-2026. 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. +@@ -36,8 +36,30 @@ the resolving functions in `m:inet`. + This DNS client can resolve DNS records even if it is not used + for normal name resolving in the node. + +-This is not a full-fledged resolver, only a DNS client that relies on asking +-trusted recursive name servers. ++> #### Warning {: .warning } ++> ++> This is not a full-fledged resolver, only a DNS client ++> that relies on being in trusted network shielded from ++> DNS reply spoofing by firewalls, and on asking only trusted ++> recursive name servers. ++> ++> If the Crypto application is loaded, this implementation ++> does its best to not be fooled by spoofed DNS replies. ++> This is nevertheless a meager remedy compared to using ++> encrypted and signed DNS methods like TSIG or DNSSEC, ++> as full fledged resolvers do. ++ ++> #### Note {: .info } ++> ++> If the Crypto application is loaded, this resolver client uses ++> cryptographically random transaction IDs and port numbers, ++> which should render the probability for a sucessfully brute force ++> spoofed reply to be too low to be a usable exploit. ++> An Erlang node that is installed in an exposed network environment ++> should already have the Crypto application loaded for example ++> to run the SSL application. If this is not the case, ++> the Crypto application can be explicitly loaded with ++> `application:load(crypto)`. + + ## Name Resolving + +@@ -73,6 +95,12 @@ For queries not using the `search` list, if the query to all `nameservers` + results in `{error,nxdomain}` or an empty answer, the same query is tried for + `alt_nameservers`. + ++If randomization of transaction ID:s and port numbers is too costly ++for an Erlang node in a trusted network shielded from DNS reply spoofing, ++the legacy behaviour to reuse the UDP socket on retries and use sequential ++transaction ID:s can be configured by setting the resolver option ++`random` to `false`. ++ + ## Resolver Types + + The following data types concern the resolver: +@@ -143,6 +171,7 @@ example_lookup(Name, Class, Type) -> + | {udp_payload_size, integer()} + | {dnssec_ok, boolean()} + | {usevc, boolean()} ++ | {random, boolean()} + | {nxdomain_reply, boolean()}. + + -type nameserver() :: {inet:ip_address(), Port :: 1..65535}. +@@ -505,7 +534,7 @@ do_nslookup(Name, Class, Type, Opts, Timeout) -> + -record(options, { % These must be sorted! + alt_nameservers,dnssec_ok,edns,inet6,nameservers, + nxdomain_reply, % this is a local option, not in inet_db +- recurse,retry,servfail_retry_timeout,timeout, ++ random,recurse,retry,servfail_retry_timeout,timeout, + udp_payload_size,usevc, + verbose}). % this is a local option, not in inet_db + %% +@@ -982,65 +1011,120 @@ make_query(Dname, Class, Type, Options, Edns) -> + Buffer = inet_dns:encode(Msg, false), + {Msg, Buffer}. + ++update_query_id(#q{ edns = EdnsQ, dns = DnsQ } = Q) -> ++ Q#q{ edns = update_query_id_part(EdnsQ), ++ dns = update_query_id_part(DnsQ) }. ++ ++update_query_id_part({#dns_rec{ header = Header } = Msg, Buffer}) -> ++ Id = inet_db:res_option(next_id), ++ {Msg#dns_rec{ header = Header#dns_header{ id = Id }}, ++ inet_dns:update_id(Buffer, Id)}; ++update_query_id_part(undefined) -> undefined; ++update_query_id_part(DnsRecFun) when is_function(DnsRecFun, 0) -> DnsRecFun. ++ + %% -------------------------------------------------------------------------- + %% socket helpers + %% + -record(sock, {inet=undefined, inet6=undefined}). + +-udp_open(#sock{inet6=I}=S, {A,B,C,D,E,F,G,H}) when ?ip6(A,B,C,D,E,F,G,H) -> ++udp_open(undefined, _IP, Verbose) when is_boolean(Verbose) -> ++ {ok, undefined}; ++udp_open(#sock{inet6=I}=S, {A,B,C,D,E,F,G,H} = IP, Verbose) ++ when ?ip6(A,B,C,D,E,F,G,H), is_boolean(Verbose) -> + case I of + undefined -> + case gen_udp:open(0, [{active,false},binary,inet6]) of + {ok,J} -> + {ok,S#sock{inet6=J}}; + Error -> ++ ?verbose(Verbose, "UDP open failed ~p ~p~n", [IP, Error]), + Error + end; + _ -> + {ok,S} + end; +-udp_open(#sock{inet=I}=S, {A,B,C,D}) when ?ip(A,B,C,D) -> ++udp_open(#sock{inet=I}=S, {A,B,C,D} = IP, Verbose) ++ when ?ip(A,B,C,D), is_boolean(Verbose) -> + case I of + undefined -> + case gen_udp:open(0, [{active,false},binary,inet]) of + {ok,J} -> + {ok,S#sock{inet=J}}; + Error -> ++ ?verbose(Verbose, "UDP open failed ~p ~p~n", [IP, Error]), + Error + end; + _ -> + {ok,S} + end. + +-udp_connect(#sock{inet6=I}, {A,B,C,D,E,F,G,H}=IP, Port) +- when ?ip6(A,B,C,D,E,F,G,H), ?port(Port) -> +- gen_udp:connect(I, IP, Port); +-udp_connect(#sock{inet=I}, {A,B,C,D}=IP, Port) +- when ?ip(A,B,C,D) -> +- gen_udp:connect(I, IP, Port). +- +-udp_send(#sock{inet6=I}, {A,B,C,D,E,F,G,H}=IP, Port, Buffer) +- when ?ip6(A,B,C,D,E,F,G,H), ?port(Port) -> +- gen_udp:send(I, IP, Port, Buffer); +-udp_send(#sock{inet=I}, {A,B,C,D}=IP, Port, Buffer) +- when ?ip(A,B,C,D), ?port(Port) -> +- gen_udp:send(I, IP, Port, Buffer). +- +-udp_recv(#sock{inet6=I}, {A,B,C,D,E,F,G,H}=IP, Port, Timeout, Decode) +- when ?ip6(A,B,C,D,E,F,G,H), ?port(Port), 0 =< Timeout -> +- do_udp_recv(I, IP, Port, Timeout, Decode, time(Timeout), Timeout); +-udp_recv(#sock{inet=I}, {A,B,C,D}=IP, Port, Timeout, Decode) +- when ?ip(A,B,C,D), ?port(Port), 0 =< Timeout -> +- do_udp_recv(I, IP, Port, Timeout, Decode, time(Timeout), Timeout). +- +-do_udp_recv(_I, _IP, _Port, 0, _Decode, _Time, PollCnt) +- when PollCnt =< 0 -> +- timeout; +-do_udp_recv(I, IP, Port, Timeout, Decode, Time, PollCnt) -> +- case gen_udp:recv(I, 0, Timeout) of +- {ok,Reply} -> +- case Decode(Reply) of +- false when Timeout =:= 0 -> ++ ++udp_connect(undefined, {A,B,C,D,E,F,G,H}=IP, Port, Verbose) ++ when ?ip6(A,B,C,D,E,F,G,H), ?port(Port), is_boolean(Verbose) -> ++ udp_connect_fam(inet6, IP, Port, Verbose); ++udp_connect(undefined, {A,B,C,D}=IP, Port, Verbose) ++ when ?ip(A,B,C,D), ?port(Port), is_boolean(Verbose) -> ++ udp_connect_fam(inet, IP, Port, Verbose); ++%% ++udp_connect(#sock{inet6=I}, {A,B,C,D,E,F,G,H}=IP, Port, Verbose) ++ when ?ip6(A,B,C,D,E,F,G,H), ?port(Port), is_boolean(Verbose) -> ++ udp_connect_socket(I, IP, Port, Verbose); ++udp_connect(#sock{inet=I}, {A,B,C,D}=IP, Port, Verbose) ++ when ?ip(A,B,C,D), ?port(Port), is_boolean(Verbose) -> ++ udp_connect_socket(I, IP, Port, Verbose). ++ ++%% Port number -1 is an internal feature that randomizes ++%% the bind port more than an ephemeral port, but it is not allowed ++%% by the type spec for gen_udp:open/2, so we have to suppress ++%% the Dialyzer warnings that that causes here... ++-dialyzer({[no_return, no_fail_call], udp_connect_fam/4}). ++udp_connect_fam(Fam, IP, Port, Verbose) -> ++ case gen_udp:open(-1, [{active,false},binary,Fam]) of ++ {ok, Socket} = OK -> ++ case gen_udp:connect(Socket, IP, Port) of ++ ok -> ++ ?verbose(Verbose, "UDP connected ~p:~p~n", ++ [IP, Port]), ++ OK; ++ {error, _} = E1 -> ++ ?verbose(Verbose, ++ "UDP connect error ~p:~p ~p~n", [IP, Port, E1]), ++ _ = gen_udp:close(Socket), ++ E1 ++ end; ++ {error, _} = E2 -> ++ ?verbose(Verbose, ++ "UDP open failed ~p:~p ~p~n", [IP, Port, E2]), ++ E2 ++ end. ++ ++udp_connect_socket(Socket, IP, Port, Verbose) -> ++ case gen_udp:connect(Socket, IP, Port) of ++ ok -> ++ ?verbose(Verbose, "UDP connected ~p:~p~n", [IP, Port]), ++ {ok, Socket}; ++ {error, _} = Error -> ++ ?verbose(Verbose, ++ "UDP connect error ~p:~p ~p~n", [IP, Port, Error]), ++ Error ++ end. ++ ++ ++udp_recv(Socket, Timeout, Decode) -> ++ PollCnt = Timeout div 50, ++ do_udp_recv(Socket, Timeout, Decode, deadline(Timeout), PollCnt). ++ ++do_udp_recv(Socket, Timeout, Decode, Deadline, PollCnt) ++ when is_integer(Timeout), 0 =< Timeout, ++ is_integer(Deadline), ++ is_integer(PollCnt), 0 < PollCnt -> ++ case gen_udp:recv(Socket, 0, Timeout) of ++ {ok,UdpMsg} -> ++ case Decode(UdpMsg) of ++ retry when 0 < Timeout -> ++ do_udp_recv( ++ Socket, timeout(Deadline), Decode, Deadline, PollCnt); ++ retry -> + %% This is a compromise between the hard way i.e + %% in the clause below if Timeout becomes 0 bailout + %% immediately and risk that the right reply lies +@@ -1051,17 +1135,20 @@ do_udp_recv(I, IP, Port, Timeout, Decode, Time, PollCnt) -> + %% DNS server flooding with bad id replies causing + %% an infinite loop here. + %% ++ %% Finalize with PollCnt number of recv timeout 0 ++ %% + do_udp_recv( +- I, IP, Port, Timeout, Decode, Time, PollCnt-50); +- false -> +- do_udp_recv( +- I, IP, Port, timeout(Time), Decode, Time, PollCnt); ++ Socket, Timeout, Decode, Deadline, PollCnt - 1); + Result -> + Result + end; + Error -> Error +- end. ++ end; ++do_udp_recv(_Socket, Timeout, _Decode, Deadline, 0) ++ when is_integer(Timeout), 0 =< Timeout, is_integer(Deadline) -> ++ timeout. + ++udp_close(undefined) -> ok; + udp_close(#sock{inet=I,inet6=I6}) -> + if I =/= undefined -> gen_udp:close(I); true -> ok end, + if I6 =/= undefined -> gen_udp:close(I6); true -> ok end, +@@ -1094,7 +1181,7 @@ udp_close(#sock{inet=I,inet6=I6}) -> + do_query(_Q, [], _Timer) -> + %% We have no name server to ask, so say nxdomain + {error,nxdomain}; +-do_query(#q{options=#options{retry=Retry}}=Q, NSs, Timer) -> ++do_query(#q{options=#options{retry=Retry,random=Random}}=Q, NSs, Timer) -> + %% We have at least one name server, + %% so a failure will be a time-out, + %% unless a name server says otherwise +@@ -1106,7 +1193,11 @@ do_query(#q{options=#options{retry=Retry}}=Q, NSs, Timer) -> + (_) -> false + end, NSs) orelse + erlang:error(badarg, [Q,NSs,Timer]), +- query_retries(Q, NSs, Timer, Retry, 0, #sock{}, Reason). ++ S = case Random of ++ true -> undefined; ++ false -> #sock{} ++ end, ++ query_retries(Q, NSs, Timer, Retry, 0, S, Reason). + + %% Loop until out of retries or name servers + %% +@@ -1193,7 +1284,7 @@ query_nss_dns( + + %% Wrap with retry time + servfail_retry_time(RetryTimeout, NS) -> +- {servfail_retry, time(RetryTimeout), NS}. ++ {servfail_retry, deadline(RetryTimeout), NS}. + + %% Unwrap and wait + servfail_retry_wait(NsSpec) -> +@@ -1224,29 +1315,29 @@ query_nss_result(Q, NSs, Timer, Retry, I, S, Reason, RetryNSs, NS, Result) -> + %% The server did not like that. + %% Do not retry this server since + %% it will not answer differently on the next retry. +- query_nss(Q, NSs, Timer, Retry, I, S, NewReason, RetryNSs); ++ query_nss_retry(Q, NSs, Timer, Retry, I, S, NewReason, RetryNSs); + {error,E=NewReason} + when E =:= formerr; + E =:= enetunreach; + E =:= econnrefused -> + %% Could not decode answer, or network problem. + %% Do not retry this server. +- query_nss(Q, NSs, Timer, Retry, I, S, NewReason, RetryNSs); ++ query_nss_retry(Q, NSs, Timer, Retry, I, S, NewReason, RetryNSs); + {error,timeout} -> % Query time-out + %% Try next server, may retry this server +- query_nss(Q, NSs, Timer, Retry, I, S, Reason, [NS|RetryNSs]); ++ query_nss_retry(Q, NSs, Timer, Retry, I, S, Reason, [NS|RetryNSs]); + {error,{servfail,_}=NewReason} -> + RetryTimeout = Q#q.options#options.servfail_retry_timeout, + case inet:timeout(RetryTimeout, Timer) of + RetryTimeout -> + NsSpec = servfail_retry_time(RetryTimeout, NS), +- query_nss( ++ query_nss_retry( + Q, NSs, Timer, Retry, I, S, NewReason, + [NsSpec|RetryNSs]); + _ -> + %% No time for a new retry with this server + %% - do not retry this server +- query_nss( ++ query_nss_retry( + Q, NSs, Timer, Retry, I, S, NewReason, RetryNSs) + end; + {error,NewReason} -> +@@ -1255,9 +1346,18 @@ query_nss_result(Q, NSs, Timer, Retry, I, S, Reason, RetryNSs, NS, Result) -> + %% {error,{noquery,Msg}} | + %% {error,OtherSocketError} + %% Try next server, may retry this server +- query_nss(Q, NSs, Timer, Retry, I, S, NewReason, [NS|RetryNSs]) ++ query_nss_retry( ++ Q, NSs, Timer, Retry, I, S, NewReason, [NS|RetryNSs]) + end. + ++query_nss_retry(Q0, NSs, Timer, Retry, I, S, Reason, RetryNSs) -> ++ Q = case S of ++ undefined -> update_query_id(Q0); ++ #sock{} -> Q0 ++ end, ++ query_nss(Q, NSs, Timer, Retry, I, S, Reason, RetryNSs). ++ ++ + query_retries_error(#q{options=#options{nxdomain_reply=NxReply}}, S, Reason) -> + _ = udp_close(S), + case Reason of +@@ -1277,7 +1377,7 @@ query_ns(S0, {Msg, Buffer}, IP, Port, Timer, Retry, I, + {S0, + query_tcp(TcpTimeout, Msg, Buffer, IP, Port, Verbose)}; + false -> +- case udp_open(S0, IP) of ++ case udp_open(S0, IP, Verbose) of + {ok,S} -> + UdpTimeout = + inet:timeout( (Tm * (1 bsl I)) div Retry, Timer), +@@ -1300,43 +1400,61 @@ query_ns(S0, {Msg, Buffer}, IP, Port, Timer, Retry, I, + end + end. + +-query_udp(_S, _Msg, _Buffer, _IP, _Port, 0, _Verbose) -> ++%% See udp_connect_fam/4 above. The use of port number -1 causes ++%% a secondary Dialyzer warning here since Dialyzer concludes ++%% that S cannot be 'undefined' because that would cause udp_connect/4 ++%% to crash here, so `S =:= undefined` can never be `true`. ++%% This is a suppression for that. ++-dialyzer({no_match, query_udp/7}). ++query_udp(_S, _Msg0, _Buffer, IP, Port, 0, Verbose) -> ++ ?verbose(Verbose, "No try UDP server : ~p:~p (overdue)\n", ++ [IP,Port]), + timeout; + query_udp(S, Msg, Buffer, IP, Port, Timeout, Verbose) -> + ?verbose(Verbose, "Try UDP server : ~p:~p (timeout=~w)\n", + [IP,Port,Timeout]), +- case +- case udp_connect(S, IP, Port) of +- ok -> +- udp_send(S, IP, Port, Buffer); +- E1 -> +- E1 end of +- ok -> +- Decode = +- fun ({RecIP,RecPort,Answer}) +- when RecIP =:= IP, RecPort =:= Port -> +- case decode_answer(Answer, Msg, Verbose) of +- {error,badid} -> +- false; +- Reply -> +- Reply +- end; +- ({_,_,_}) -> +- false +- end, +- case udp_recv(S, IP, Port, Timeout, Decode) of +- {ok,_}=Result -> +- Result; +- E2 -> +- ?verbose(Verbose, "UDP server error: ~p\n", [E2]), +- E2 +- end; +- E3 -> +- ?verbose(Verbose, "UDP send failed: ~p\n", [E3]), +- {error,econnrefused} ++ case udp_connect(S, IP, Port, Verbose) of ++ {error, _} = E1 -> ++ ?verbose(Verbose, "UDP connect failed: ~p\n", [E1]), ++ E1; ++ {ok, Socket} -> ++ DecodeFun = ++ fun ({RecIP, RecPort, Bin}) ++ when RecIP =:= IP, RecPort =:= Port -> ++ case decode_reply(Bin, Msg, Verbose) of ++ {error,Reason} ++ when Reason =:= badid; ++ Reason =:= unknown; ++ Reason =:= noquery -> retry; ++ Reply -> Reply ++ end; ++ ({_, _, _}) -> retry ++ end, ++ try ++ case gen_udp:send(Socket, Buffer) of ++ {error, _} = E2 -> ++ ?verbose(Verbose, "UDP send failed: ~p ~p ~p\n", ++ [Socket, Buffer, E2]), ++ E2; ++ ok -> ++ case udp_recv(Socket, Timeout, DecodeFun) of ++ {ok, _} = Result -> Result; ++ E3 -> ++ ?verbose(Verbose, ++ "UDP server error: ~p\n", [E3]), ++ E3 ++ end ++ end ++ after ++ S =:= undefined andalso ++ gen_udp:close(Socket) ++ end + end. + +-query_tcp(0, _Msg, _Buffer, _IP, _Port, _Verbose) -> ++ ++query_tcp(0, _Msg, _Buffer, IP, Port, Verbose) -> ++ ?verbose(Verbose, "No try TCP server : ~p:~p (overdue)\n", ++ [IP, Port]), + timeout; + query_tcp(Timeout, Msg, Buffer, IP, Port, Verbose) -> + ?verbose(Verbose, "Try TCP server : ~p:~p (timeout=~w)\n", +@@ -1352,9 +1470,9 @@ query_tcp(Timeout, Msg, Buffer, IP, Port, Verbose) -> + case gen_tcp:send(S, Buffer) of + ok -> + case gen_tcp:recv(S, 0, Timeout) of +- {ok, Answer} -> ++ {ok, Bin} -> + gen_tcp:close(S), +- case decode_answer(Answer, Msg, Verbose) of ++ case decode_reply(Bin, Msg, Verbose) of + {ok, _} = OK -> OK; + {error, badid} -> {error, servfail}; + Error -> Error +@@ -1378,8 +1496,8 @@ query_tcp(Timeout, Msg, Buffer, IP, Port, Verbose) -> + _:_ -> {error, einval} + end. + +-decode_answer(Answer, Q_Msg, Verbose) -> +- case inet_dns:decode(Answer, false) of ++decode_reply(Bin, Q_Msg, Verbose) -> ++ case inet_dns:decode_reply(Bin, Q_Msg, false) of + {ok, #dns_rec{header = H, arlist = ARList} = Msg} -> + ?verbose(Verbose, "Got reply: ~p~n", [dns_msg(Msg)]), + T = case lists:keyfind(dns_rr_tsig, 1, ARList) of +@@ -1393,7 +1511,8 @@ decode_answer(Answer, Q_Msg, Verbose) -> + end, + RCode = T orelse (E bsl 4) bor H#dns_header.rcode, + case RCode of +- ?NOERROR -> decode_answer_noerror(Q_Msg, Msg, H); ++ ?NOERROR -> {ok,Msg}; ++ %% decode_answer_noerror(Q_Msg, Msg, H); + ?FORMERR -> {error,{qfmterror,Msg}}; + ?SERVFAIL -> {error,{servfail,Msg}}; + ?NXDOMAIN -> {error,{nxdomain,Msg}}; +@@ -1411,44 +1530,11 @@ decode_answer(Answer, Q_Msg, Verbose) -> + ?BADTRUNC -> {error,{badtrunc,Msg}}; + _ -> {error,{unknown,Msg}} + end; +- {error, formerr} = Error -> +- ?verbose(Verbose, "Got reply: decode format error~n", []), ++ {error, Reason} = Error -> ++ ?verbose(Verbose, "Got reply: decode error ~p~n", [Reason]), + Error + end. + +-decode_answer_noerror( +- #dns_rec{header = Q_H, qdlist = [Q_RR]}, +- #dns_rec{qdlist = QDList} = Msg, +- H) -> +- %% Validate the reply +- if +- H#dns_header.id =/= Q_H#dns_header.id -> +- {error,badid}; +- H#dns_header.qr =/= true; +- H#dns_header.opcode =/= Q_H#dns_header.opcode; +- H#dns_header.rd andalso not Q_H#dns_header.rd -> +- {error,{unknown,Msg}}; +- true -> +- case QDList of +- [RR] -> +- case +- (RR#dns_query.class =:= Q_RR#dns_query.class) +- andalso +- (RR#dns_query.type =:= Q_RR#dns_query.type) +- andalso +- inet_db:eq_domains( +- RR#dns_query.domain, Q_RR#dns_query.domain) +- of +- true -> +- {ok, Msg}; +- false -> +- {error,{noquery,Msg}} +- end; +- _ when is_list(QDList) -> +- {error,{noquery,Msg}} +- end +- end. +- + %% + %% Transform domain name or address + %% 1. "a.b.c" => +@@ -1552,20 +1638,20 @@ dns_msg(Msg) -> + + + +--compile({inline, [time/1, timeout/1, wait/1]}). ++-compile({inline, [deadline/1, timeout/1, wait/1]}). + +-%% What Time is the Timeout? [ms] ++%% When is the Timeout? [ms] + %% +-time(Timeout) -> ++deadline(Timeout) when is_integer(Timeout), 0 =< Timeout -> + erlang:monotonic_time(1000) + Timeout. + +-%% How long Timeout to Time? [ms] >= 0 ++%% How long Timeout to Deadline? [ms] >= 0 + %% +-timeout(Time) -> +- TimeNow = erlang:monotonic_time(1000), ++timeout(Deadline) when is_integer(Deadline) -> ++ Time = erlang:monotonic_time(1000), + if +- TimeNow < Time -> +- Time - TimeNow; ++ Time < Deadline -> ++ Deadline - Time; + true -> + 0 + end. +@@ -1574,7 +1660,7 @@ timeout(Time) -> + %% + wait(0) -> + ok; +-wait(Timeout) -> ++wait(Timeout) when is_integer(Timeout), 0 < Timeout -> + receive + after Timeout -> + ok +diff --git a/lib/kernel/src/inet_sctp.erl b/lib/kernel/src/inet_sctp.erl +index 2acaf213ab..f299152e22 100644 +--- a/lib/kernel/src/inet_sctp.erl ++++ b/lib/kernel/src/inet_sctp.erl +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2007-2024. All Rights Reserved. ++%% Copyright Ericsson AB 2007-2026. 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. +@@ -55,10 +55,12 @@ open(Opts) -> + ifaddr = Addr, + port = Port, + type = Type, +- opts = SOs}} -> ++ opts = SOs}} ++ when ?port(Port) -> + inet:open_bind( + Fd, Addr, Port, SOs, ?PROTO, ?FAMILY, Type, ?MODULE); +- Error -> Error ++ {ok, _} -> {error, badarg}; ++ Error -> Error + end. + + close(S) -> +diff --git a/lib/kernel/src/inet_udp.erl b/lib/kernel/src/inet_udp.erl +index 7c1039bce8..750d020d21 100644 +--- a/lib/kernel/src/inet_udp.erl ++++ b/lib/kernel/src/inet_udp.erl +@@ -1,8 +1,8 @@ + %% + %% %CopyrightBegin% +-%% +-%% Copyright Ericsson AB 1997-2024. All Rights Reserved. +-%% ++%% ++%% Copyright Ericsson AB 1997-2026. 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. + %% You may obtain a copy of the License at +@@ -62,6 +62,9 @@ open(Port, Opts) -> + port = BPort, + opts = SockOpts}} + when is_map(BAddr); % sockaddr_in() ++ %% ++ BPort =:= -1, ?ip(BAddr); ++ BPort =:= -1, BAddr =:= undefined; + ?port(BPort), ?ip(BAddr); + ?port(BPort), BAddr =:= undefined -> + inet:open_bind( +diff --git a/lib/kernel/test/inet_res_SUITE.erl b/lib/kernel/test/inet_res_SUITE.erl +index c62c5ca028..f22900113b 100644 +--- a/lib/kernel/test/inet_res_SUITE.erl ++++ b/lib/kernel/test/inet_res_SUITE.erl +@@ -1,7 +1,7 @@ + %% + %% %CopyrightBegin% + %% +-%% Copyright Ericsson AB 2009-2024. All Rights Reserved. ++%% Copyright Ericsson AB 2009-2026. 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. +@@ -150,6 +150,8 @@ zone_dir(TC) -> + end. + + init_per_testcase(Func, Config) -> ++ _ = application:load(crypto), % Enable DNS request ID and port randomness ++ %% inet_db:res_option(random, false), % Disable the above + + ?P("init_per_testcase -> entry with" + "~n Func: ~p" +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-32144-1.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-32144-1.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-32144-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-32144-1.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,195 @@ +From b58640193dd078e36d532d41c1c683379acc5ca3 Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Tue, 7 Apr 2026 09:36:40 +0200 +Subject: [PATCH] Merge branch + 'kuba/maint-27/public_key/ocsp_responder_fix/OTP-20042' into maint-27 + +* kuba/maint-27/public_key/ocsp_responder_fix/OTP-20042: + public_key: Verify designated OCSP responder certificate signature + +(cherry picked from commit 0585bde5255272250fdf1f69753d69a819ebc60f) +--- + lib/public_key/src/pubkey_ocsp.erl | 4 +- + lib/public_key/test/pubkey_ocsp_SUITE.erl | 147 +++++++++++++++++++++- + 2 files changed, 149 insertions(+), 2 deletions(-) + +diff --git a/lib/public_key/src/pubkey_ocsp.erl b/lib/public_key/src/pubkey_ocsp.erl +index 8e343f3ab3..89355e8937 100644 +--- a/lib/public_key/src/pubkey_ocsp.erl ++++ b/lib/public_key/src/pubkey_ocsp.erl +@@ -229,7 +229,9 @@ is_authorized_responder(CombinedResponderCert = #cert{otp = ResponderCert}, + %% issue OCSP responses for that CA (id-kp-OCSPSigning) + fun() -> + public_key:pkix_is_issuer(ResponderCert, IssuerCert) andalso +- designated_for_ocsp_signing(ResponderCert) ++ designated_for_ocsp_signing(ResponderCert) andalso ++ public_key:pkix_verify(CombinedResponderCert#cert.der, ++ get_public_key_rec(IssuerCert)) + end, + Case3 = + %% a Trusted Responder whose public key is trusted by the requestor +diff --git a/lib/public_key/test/pubkey_ocsp_SUITE.erl b/lib/public_key/test/pubkey_ocsp_SUITE.erl +index 8082cc6ce6..480d201c10 100644 +--- a/lib/public_key/test/pubkey_ocsp_SUITE.erl ++++ b/lib/public_key/test/pubkey_ocsp_SUITE.erl +@@ -105,7 +105,7 @@ + %% Common Test interface functions ----------------------------------- + %%-------------------------------------------------------------------- + all() -> +- [ocsp_test]. ++ [ocsp_test, designated_responder]. + + groups() -> + []. +@@ -211,3 +211,148 @@ ocsp_test(Config) when is_list(Config) -> + ?ISSUER_CERT, + IsTrustedReponderFun), + ok. ++ ++%%-------------------------------------------------------------------- ++designated_responder() -> ++ [{doc, "Test Case2 (designated responder) in is_authorized_responder/3. " ++ "Verifies that a legitimate designated responder cert signed by the CA " ++ "is accepted, and a forged self-signed cert with the same subject DN " ++ "is rejected (GHSA-gxrm-pf64-99xm)."}]. ++designated_responder(Config) when is_list(Config) -> ++ %% EC keys are used for fast key generation (vs RSA-2048). ++ %% is_authorized_responder/3 Case 2 only checks DN match, ++ %% OCSPSigning EKU, and pkix_verify — no chain validation. ++ CAKey = public_key:generate_key({namedCurve, ?'secp256r1'}), ++ CAPubKey = ec_public_key(CAKey), ++ CASubject = cn_subject(<<"Test CA">>), ++ CACertDer = public_key:pkix_sign(ca_tbs(CASubject, CAPubKey), CAKey), ++ CACert = public_key:pkix_decode_cert(CACertDer, otp), ++ ++ %% Legitimate designated responder cert (signed by CA) ++ ResponderKey = public_key:generate_key({namedCurve, ?'secp256r1'}), ++ {ResponderCertDer, ResponderCert} = ++ sign_responder_cert(2, CASubject, ec_public_key(ResponderKey), CAKey), ++ ++ %% Forged designated responder (self-signed, same subject DN) ++ ForgedKey = public_key:generate_key({namedCurve, ?'secp256r1'}), ++ {ForgedCertDer, ForgedCert} = ++ sign_responder_cert(9999, CASubject, ec_public_key(ForgedKey), ForgedKey), ++ ++ %% Build OCSP responses and verify ++ Nonce = crypto:strong_rand_bytes(8), ++ NonceExt = <<4, 8, Nonce/binary>>, ++ IsNotTrustedFun = fun(_) -> false end, ++ ++ %% Positive: legitimate designated responder accepted ++ LegitResponse = build_ocsp_response(CASubject, CAKey, NonceExt, ResponderKey), ++ {ok, [_], _} = ++ pubkey_ocsp:verify_response( ++ LegitResponse, ++ [#cert{otp = ResponderCert, der = ResponderCertDer}], ++ NonceExt, CACert, IsNotTrustedFun), ++ ++ %% Negative: forged responder (same DN, not signed by CA) rejected ++ ForgedResponse = build_ocsp_response(CASubject, CAKey, NonceExt, ForgedKey), ++ {error, ocsp_responder_cert_not_found} = ++ pubkey_ocsp:verify_response( ++ ForgedResponse, ++ [#cert{otp = ForgedCert, der = ForgedCertDer}], ++ NonceExt, CACert, IsNotTrustedFun), ++ ok. ++ ++%%-------------------------------------------------------------------- ++%% Helpers ----------------------------------------------------------- ++%%-------------------------------------------------------------------- ++ ++ec_public_key(#'ECPrivateKey'{publicKey = PubKey}) -> ++ #'ECPoint'{point = PubKey}. ++ ++cn_subject(CN) -> ++ {rdnSequence, ++ [[#'AttributeTypeAndValue'{ ++ type = ?'id-at-commonName', ++ value = {utf8String, CN}}]]}. ++ ++ec_subject_pubkey_info(PubKey) -> ++ #'OTPSubjectPublicKeyInfo'{ ++ algorithm = #'PublicKeyAlgorithm'{ ++ algorithm = ?'id-ecPublicKey', ++ parameters = {namedCurve, ?'secp256r1'}}, ++ subjectPublicKey = PubKey}. ++ ++ca_tbs(Subject, PubKey) -> ++ make_tbs(1, Subject, PubKey, ++ [#'Extension'{extnID = ?'id-ce-basicConstraints', ++ critical = true, ++ extnValue = #'BasicConstraints'{cA = true}}, ++ #'Extension'{extnID = ?'id-ce-keyUsage', ++ critical = false, ++ extnValue = [keyCertSign, cRLSign]}]). ++ ++ocsp_responder_tbs(Serial, Issuer, PubKey) -> ++ make_tbs(Serial, Issuer, PubKey, ++ [#'Extension'{extnID = ?'id-ce-basicConstraints', ++ critical = false, ++ extnValue = #'BasicConstraints'{cA = false}}, ++ #'Extension'{extnID = ?'id-ce-keyUsage', ++ critical = false, ++ extnValue = [digitalSignature]}, ++ #'Extension'{extnID = ?'id-ce-extKeyUsage', ++ critical = false, ++ extnValue = [?'id-kp-OCSPSigning']}]). ++ ++sign_responder_cert(Serial, Issuer, PubKey, SigningKey) -> ++ Der = public_key:pkix_sign(ocsp_responder_tbs(Serial, Issuer, PubKey), SigningKey), ++ {Der, public_key:pkix_decode_cert(Der, otp)}. ++ ++make_tbs(Serial, Subject, PubKey, Extensions) -> ++ #'OTPTBSCertificate'{ ++ version = v3, ++ serialNumber = Serial, ++ signature = #'SignatureAlgorithm'{ ++ algorithm = ?'ecdsa-with-SHA256', ++ parameters = asn1_NOVALUE}, ++ issuer = Subject, ++ validity = #'Validity'{ ++ notBefore = {utcTime, "240101000000Z"}, ++ notAfter = {utcTime, "340101000000Z"}}, ++ subject = Subject, ++ subjectPublicKeyInfo = ec_subject_pubkey_info(PubKey), ++ extensions = Extensions}. ++ ++build_ocsp_response(IssuerName, IssuerKey, NonceExt, SignKey) -> ++ EncodedName = pubkey_cert_records:transform(IssuerName, encode), ++ IssuerNameHash = crypto:hash(sha, public_key:der_encode('Name', EncodedName)), ++ IssuerKeyHash = crypto:hash(sha, IssuerKey#'ECPrivateKey'.publicKey), ++ ResponseData = #'ResponseData'{ ++ version = v1, ++ responderID = {byName, EncodedName}, ++ producedAt = "20250101000000Z", ++ responses = ++ [#'SingleResponse'{ ++ certID = #'CertID'{ ++ hashAlgorithm = #'AlgorithmIdentifier'{ ++ algorithm = ?'id-sha1', ++ parameters = <<5,0>>}, ++ issuerNameHash = IssuerNameHash, ++ issuerKeyHash = IssuerKeyHash, ++ serialNumber = 100}, ++ certStatus = {good, 'NULL'}, ++ thisUpdate = "20250101000000Z", ++ nextUpdate = asn1_NOVALUE, ++ singleExtensions = asn1_NOVALUE}], ++ responseExtensions = ++ [#'Extension'{ ++ extnID = ?'id-pkix-ocsp-nonce', ++ critical = false, ++ extnValue = NonceExt}]}, ++ ResponseDataDer = public_key:der_encode('ResponseData', ResponseData), ++ Signature = public_key:sign(ResponseDataDer, sha256, SignKey), ++ #'BasicOCSPResponse'{ ++ tbsResponseData = ResponseData, ++ signatureAlgorithm = ++ #'AlgorithmIdentifier'{ ++ algorithm = ?'ecdsa-with-SHA256', ++ parameters = asn1_NOVALUE}, ++ signature = Signature, ++ certs = asn1_NOVALUE}. +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-32144-2.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-32144-2.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-32144-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-32144-2.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,387 @@ +From 0530e134f1fc4b1a38ec29f5fe73cebac25e0a95 Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Tue, 7 Apr 2026 09:36:40 +0200 +Subject: [PATCH] Merge branch 'kuba/maint-27/ssl/stapling_hard_fail/OTP-20064' + into maint-27 + +* kuba/maint-27/ssl/stapling_hard_fail/OTP-20064: + ssl: Hard-fail on missing OCSP staple when stapling is configured + +(cherry picked from commit 8b8bbd22fd97972bf0bf1373a3d3c67bf224c08f) + +Debian note: the hunk touching lib/ssl/src/ssl.app.src is dropped. In upstream +git that runtime_dependencies entry is the unsubstituted placeholder +"public_key-@OTP-NNNNN@", which upstream's release process resolves before the +tarball is rolled; the released 27.3.4.1 tarball already carries the literal +"public_key-1.16.4". Applying it here would write an unresolved placeholder +into the shipped ssl.app. +--- + lib/ssl/src/ssl.erl | 29 ++++++++- + lib/ssl/src/ssl_certificate.erl | 30 +++------- + lib/ssl/src/ssl_handshake.erl | 20 +++++-- + lib/ssl/src/tls_dtls_client_connection.erl | 17 ++++-- + lib/ssl/src/tls_handshake_1_3.erl | 34 ++++++++--- + lib/ssl/test/openssl_stapling_SUITE.erl | 68 ++++++++++++++++++---- + 6 files changed, 143 insertions(+), 55 deletions(-) + +diff --git a/lib/ssl/src/ssl.erl b/lib/ssl/src/ssl.erl +index f57453d4ac..228aa4a595 100644 +--- a/lib/ssl/src/ssl.erl ++++ b/lib/ssl/src/ssl.erl +@@ -1514,9 +1514,32 @@ different semantics for the client and server. + > #### Note {: .info } + > + > Even if requested by the client, the OCSP response might not be +- > provided by the server. In such event, SSL will proceed with +- > the handshake and generate a `{missing, stapling_response}` logger +- > event. ++ > provided by the server. In such event, the certificate validation ++ > will fail with reason `missing_ocsp_staple`. Capturing this ++ > failure with a custom `verify_fun` enables the user to implement ++ > their own fallback validation, for example by performing a direct ++ > OCSP query or a CRL check. Note however that accepting ++ > `{bad_cert, missing_ocsp_staple}` without performing alternative ++ > revocation checking is insecure, as it allows a MITM attacker to ++ > suppress revocation information by omitting the OCSP staple. ++ > ++ > ```erlang ++ > {verify_fun, {fun(_, _, {bad_cert, missing_ocsp_staple} = R, _St) -> ++ > %% Implement fallback revocation check here, ++ > %% e.g. a direct OCSP query or CRL check. ++ > %% Simply returning {valid, St} skips ++ > %% revocation checking entirely. ++ > {fail, R}; ++ > (_, _, {bad_cert, _} = R, _) -> ++ > {fail, R}; ++ > (_, _, {extension, _}, St) -> ++ > {unknown, St}; ++ > (_, _, valid, St) -> ++ > {valid, St}; ++ > (_, _, valid_peer, St) -> ++ > {valid, St} ++ > end, []}} ++ > ``` + + When `Stapling` is given as a map, boolean `ocsp_nonce` key can + indicate whether an OCSP nonce should be requested by the client +diff --git a/lib/ssl/src/ssl_certificate.erl b/lib/ssl/src/ssl_certificate.erl +index 5723ab8730..e2a1286c9c 100644 +--- a/lib/ssl/src/ssl_certificate.erl ++++ b/lib/ssl/src/ssl_certificate.erl +@@ -591,32 +591,19 @@ verify_hostname(Hostname, Customize, Cert, UserState) -> + verify_cert_extensions(Cert, #{cert_ext := CertExts} = UserState, LogLevel) -> + Id = public_key:pkix_subject_id(Cert), + Extensions = maps:get(Id, CertExts, []), +- verify_cert_extensions(Cert, UserState, Extensions, +- #{certificate_valid => false}, LogLevel). +- +-verify_cert_extensions(_Cert, UserState = #{stapling_state := #{configured := true}, +- path_len := 0}, [], +- _Context = #{certificate_valid := false}, LogLevel) -> +- %% RFC6066 section 8 +- %% Servers that receive a client hello containing the "status_request" +- %% extension MAY return a suitable certificate status response to the +- %% client along with their certificate. +- Desc = "Certificate Status - stapling response not provided by the server", +- ssl_logger:log(notice, LogLevel, #{description => Desc, +- reason => [{missing, stapling_response}]}, +- ?LOCATION), +- {valid, UserState}; +-verify_cert_extensions(Cert, UserState, [], _, _) -> ++ verify_cert_extensions(Cert, UserState, Extensions, LogLevel). ++ ++verify_cert_extensions(Cert, UserState, [], _) -> + {valid, UserState#{issuer => Cert}}; + verify_cert_extensions(_, #{stapling_state := #{configured := false}}, +- [#certificate_status{} | _], _, _) -> ++ [#certificate_status{} | _], _) -> + {fail, unexpected_certificate_status}; + verify_cert_extensions(Cert, #{stapling_state := StaplingState, + issuer := Issuer, + certdb := CertDbHandle, + certdb_ref := CertDbRef} = UserState, + [#certificate_status{response = OcspResponseDer} | Exts], +- Context, LogLevel) -> ++ LogLevel) -> + #{ocsp_nonce := Nonce} = StaplingState, + IsTrustedResponderFun = + fun(#cert{der = DerResponderCert, otp = OtpCert}) -> +@@ -648,14 +635,13 @@ verify_cert_extensions(Cert, #{stapling_state := StaplingState, + H(Rest); + H([]) -> ok end, + HandleOcspDetails(Details), +- verify_cert_extensions(Cert, UserState, Exts, +- Context#{certificate_valid => true}, LogLevel); ++ verify_cert_extensions(Cert, UserState, Exts, LogLevel); + {error, {bad_cert, _} = Reason} -> + {fail, Reason} + end; +-verify_cert_extensions(Cert, UserState, [_|Exts], Context, LogLevel) -> ++verify_cert_extensions(Cert, UserState, [_|Exts], LogLevel) -> + %% Skip unknown extensions! +- verify_cert_extensions(Cert, UserState, Exts, Context, LogLevel). ++ verify_cert_extensions(Cert, UserState, Exts, LogLevel). + + verify_sign_support(_, #{version := Version}) + when ?TLS_LT(Version, ?TLS_1_2) -> +diff --git a/lib/ssl/src/ssl_handshake.erl b/lib/ssl/src/ssl_handshake.erl +index 9cb75753ec..a96ba21034 100644 +--- a/lib/ssl/src/ssl_handshake.erl ++++ b/lib/ssl/src/ssl_handshake.erl +@@ -2010,9 +2010,14 @@ extension_value(#psk_key_exchange_modes{ke_modes = Modes}) -> + extension_value(#cookie{cookie = Cookie}) -> + Cookie. + ++%% Extension value mapping (from decode_extensions/4): ++%% 'false' - extension absent from ServerHello (maps:get default) ++%% 'undefined' - extension present with empty body (RFC 6066: server ++%% MUST include status_request with empty extension_data ++%% to indicate willingness to send CertificateStatus) + handle_cert_status_extension(#{stapling := _Stapling}, Extensions) -> + case maps:get(status_request, Extensions, false) of +- undefined -> %% status_request received in server hello ++ undefined -> + #{configured => true, + status => negotiated}; + false -> +@@ -2296,11 +2301,14 @@ cert_status_check(_OtpCert, + status := StaplingStatus}}, + _VerifyResult, _CertPath, _LogLevel) + when StaplingStatus == not_negotiated; StaplingStatus == not_received -> +- %% RFC6066 section 8 +- %% Servers that receive a client hello containing the "status_request" +- %% extension MAY return a suitable certificate status response to the +- %% client along with their certificate. +- valid. ++ %% Hard-fail (TLS 1.2 and TLS 1.3): client requested OCSP stapling ++ %% but server did not provide a staple. Erlang/OTP has no fallback ++ %% to direct OCSP queries or CRL checking when stapling is ++ %% configured, so accepting a missing staple would skip revocation ++ %% checking entirely. ++ %% Note: {bad_cert, _} tuple is required for apply_user_fun/8 to ++ %% deliver the failure to a custom verify_fun. ++ {bad_cert, missing_ocsp_staple}. + + + maybe_check_crl(_, #{crl_check := false}, _, _, _) -> +diff --git a/lib/ssl/src/tls_dtls_client_connection.erl b/lib/ssl/src/tls_dtls_client_connection.erl +index 7f63dd7cdd..41b19bb565 100644 +--- a/lib/ssl/src/tls_dtls_client_connection.erl ++++ b/lib/ssl/src/tls_dtls_client_connection.erl +@@ -130,7 +130,11 @@ wait_stapling(internal, #certificate_status{} = CertStatus, + stapling_state = + StaplingState#{status => received_staple, + staple => CertStatus}}}}; +-%% Server did not send OCSP staple message ++%% TLS 1.2 only: server negotiated stapling (included status_request ++%% in ServerHello) but sent a different message instead of ++%% CertificateStatus. Mark as not_received and postpone the message ++%% for the certify state. Hard-fail is enforced later by ++%% cert_status_check/5 in ssl_handshake. + wait_stapling(internal, Msg, + #state{static_env = #static_env{protocol_cb = _Connection}, + handshake_env = #handshake_env{ +@@ -176,9 +180,11 @@ certify(internal, #certificate{asn1_certificates = DerCerts}, + connection_env = #connection_env{ + negotiated_version = Version}, + ssl_options = Opts} = State) +- when StaplingStatus == not_negotiated; StaplingStatus == received_staple -> +- %% this clause handles also scenario with stapling disabled, so +- %% 'not_negotiated' appears in guard ++ when StaplingStatus == not_negotiated; StaplingStatus == received_staple; ++ StaplingStatus == not_received -> ++ %% not_negotiated covers two cases: stapling disabled (configured=false) ++ %% and stapling enabled but server did not include status_request in ++ %% ServerHello. Hard-fail for the latter is enforced by cert_status_check/5. + Certs = try [#cert{der=DerCert, otp=public_key:pkix_decode_cert(DerCert, otp)} + || DerCert <- DerCerts] + catch +@@ -866,6 +872,7 @@ ext_info(#{status := received_staple, staple := CertStatus} = StaplingState, + #cert{otp = PeerCert}) -> + #{cert_ext => #{public_key:pkix_subject_id(PeerCert) => [CertStatus]}, + stapling_state => StaplingState}; +-ext_info(#{status := not_negotiated} = StaplingState, #cert{otp = PeerCert}) -> ++ext_info(#{status := StaplingStatus} = StaplingState, #cert{otp = PeerCert}) ++ when StaplingStatus == not_negotiated; StaplingStatus == not_received -> + #{cert_ext => #{public_key:pkix_subject_id(PeerCert) => []}, + stapling_state => StaplingState}. +diff --git a/lib/ssl/src/tls_handshake_1_3.erl b/lib/ssl/src/tls_handshake_1_3.erl +index 5e64247467..f1c03ba46a 100644 +--- a/lib/ssl/src/tls_handshake_1_3.erl ++++ b/lib/ssl/src/tls_handshake_1_3.erl +@@ -833,7 +833,9 @@ validate_certificate_chain(CertEntries, CertDbHandle, CertDbRef, + ssl_handshake:certify(Certs, CertDbHandle, + CertDbRef, SslOptions, CRLDbHandle, Role, Host, ?TLS_1_3, + ExtInfo) +- catch error:{_,{error, {asn1, Asn1Reason}}}=Reason:ST -> ++ catch throw:#alert{} = Alert -> ++ Alert; ++ error:{_,{error, {asn1, Asn1Reason}}}=Reason:ST -> + %% ASN-1 decode of certificate somehow failed + ?SSL_LOG(info, asn1_decode, [Reason, {stacktrace, ST}]), + ?ALERT_REC(?FATAL, ?CERTIFICATE_UNKNOWN, {failed_to_decode_certificate, Asn1Reason}) +@@ -856,13 +858,29 @@ split_cert_entries([#certificate_entry{data = DerCert, + + Id = public_key:pkix_subject_id(DerCert), + Extensions = [ExtValue || {_, ExtValue} <- maps:to_list(Extensions0)], +- StaplingState = case {maps:get(status_request, Extensions0, undefined), +- StaplingConfigured} of +- {undefined, _} -> +- StaplingState0; +- {_, true} -> +- StaplingState0#{status => received_staple} +- end, ++ StaplingState = ++ case {maps:get(status_request, Extensions0, undefined), StaplingConfigured} of ++ {undefined, _} -> ++ %% No OCSP response in this cert entry. ++ %% For intermediate CA certs this is normal. ++ %% For the peer cert, state stays not_negotiated ++ %% and cert_status_check/5 will hard-fail when ++ %% stapling is configured. ++ StaplingState0; ++ {#certificate_status{}, true} -> ++ %% Server provided OCSP staple and client ++ %% requested it — mark as received. The ++ %% response will be verified later by ++ %% ssl_certificate:verify_cert_extensions/4. ++ StaplingState0#{status => received_staple}; ++ {#certificate_status{}, false} -> ++ %% Unsolicited OCSP staple — client did not ++ %% configure stapling. Protocol violation per ++ %% RFC 8446 4.2: server MUST NOT include ++ %% extensions not offered in ClientHello. ++ throw(?ALERT_REC(?FATAL, ?UNSUPPORTED_EXTENSION, ++ unexpected_certificate_status)) ++ end, + split_cert_entries(CertEntries, StaplingState, [Cert | Chain], + CertExt#{Id => Extensions}). + +diff --git a/lib/ssl/test/openssl_stapling_SUITE.erl b/lib/ssl/test/openssl_stapling_SUITE.erl +index 4ae993b0bd..3cb74386af 100644 +--- a/lib/ssl/test/openssl_stapling_SUITE.erl ++++ b/lib/ssl/test/openssl_stapling_SUITE.erl +@@ -43,7 +43,9 @@ + staple_with_nonce/0, staple_with_nonce/1, + cert_status_revoked/0, cert_status_revoked/1, + cert_status_undetermined/0, cert_status_undetermined/1, +- staple_missing/0, staple_missing/1 ++ staple_missing/0, staple_missing/1, ++ staple_missing_atom/0, staple_missing_atom/1, ++ staple_missing_verify_fun/0, staple_missing_verify_fun/1 + ]). + + %% spawn export +@@ -83,7 +85,9 @@ negative() -> + staple_wrong_issuer, + cert_status_revoked, + cert_status_undetermined, +- staple_missing]. ++ staple_missing, ++ staple_missing_atom, ++ staple_missing_verify_fun]. + + %%-------------------------------------------------------------------- + init_per_suite(Config0) -> +@@ -192,17 +196,13 @@ staple_with_nonce(Config) + when is_list(Config) -> + stapling_helper(Config, #{ocsp_nonce => true}). + +-staple_missing() -> +- [{doc, "Verify OCSP stapling works with a missing OCSP response."}]. +-staple_missing(Config) +- when is_list(Config) -> +- %% Start a server that will not include an OCSP response. +- stapling_helper(Config, openssl, #{ocsp_nonce => true}). +- + stapling_helper(Config, StaplingOpt) -> + stapling_helper(Config, openssl_ocsp, StaplingOpt). + + stapling_helper(Config, ServerType, StaplingOpt) -> ++ stapling_helper(Config, ServerType, StaplingOpt, []). ++ ++stapling_helper(Config, ServerType, StaplingOpt, ExtraClientOpts) -> + %% ok = logger:set_application_level(ssl, debug), + PrivDir = proplists:get_value(priv_dir, Config), + CACertsFile = filename:join(PrivDir, "a.server/cacerts.pem"), +@@ -216,7 +216,7 @@ stapling_helper(Config, ServerType, StaplingOpt) -> + ClientOpts = ssl_test_lib:ssl_options([{verify, verify_peer}, + {cacertfile, CACertsFile}, + {server_name_indication, disable}, +- {stapling, StaplingOpt}], ++ {stapling, StaplingOpt}] ++ ExtraClientOpts, + Config), + Client = ssl_test_lib:start_client(erlang, + [{port, Port}, +@@ -228,6 +228,49 @@ stapling_helper(Config, ServerType, StaplingOpt) -> + ssl_test_lib:close(Client). + + %%-------------------------------------------------------------------- ++staple_missing() -> ++ [{doc, "Verify that missing OCSP staple causes handshake failure."}]. ++staple_missing(Config) ++ when is_list(Config) -> ++ %% Start a server (openssl) that will not include an OCSP response ++ stapling_negative_helper(Config, "a.server/cacerts.pem", ++ openssl, handshake_failure). ++ ++staple_missing_atom() -> ++ [{doc, "Verify that missing OCSP staple causes handshake failure " ++ "with {stapling, staple} atom shorthand."}]. ++staple_missing_atom(Config) ++ when is_list(Config) -> ++ stapling_negative_helper(Config, "a.server/cacerts.pem", ++ openssl, handshake_failure, staple). ++ ++staple_missing_verify_fun() -> ++ [{doc, "Verify that a custom verify_fun can be used for accepting missing OCSP staple."}]. ++staple_missing_verify_fun(Config) ++ when is_list(Config) -> ++ VerifyFun = ++ {fun(_, _, {bad_cert, missing_ocsp_staple}, UserState) -> ++ ?CT_LOG("verify_fun: got missing_ocsp_staple, accepting", []), ++ {valid, [staple_missing_seen | UserState]}; ++ (_, _, {bad_cert, _} = Reason, _) -> ++ ?CT_LOG("verify_fun: got ~p, rejecting", [Reason]), ++ {fail, Reason}; ++ (_, _, {extension, _} = Ext, UserState) -> ++ ?CT_LOG("verify_fun: got extension ~p", [Ext]), ++ {unknown, UserState}; ++ (_, _, valid, UserState) -> ++ ?CT_LOG("verify_fun: got valid", []), ++ {valid, UserState}; ++ (_, _, valid_peer, UserState) -> ++ ?CT_LOG("verify_fun: got valid_peer, state=~p", [UserState]), ++ case lists:member(staple_missing_seen, UserState) of ++ true -> {valid, UserState}; ++ false -> {fail, {bad_cert, missing_staple_not_reported}} ++ end ++ end, []}, ++ stapling_helper(Config, openssl, #{ocsp_nonce => false}, ++ [{verify_fun, VerifyFun}]). ++ + staple_not_designated() -> + [{doc,"Verify OCSP stapling works without nonce." + "Response signed with certificate issued directly by issuer of server " +@@ -262,6 +305,9 @@ cert_status_undetermined(Config) + openssl_ocsp_undetermined, bad_certificate). + + stapling_negative_helper(Config, CACertsPath, ServerVariant, ExpectedError) -> ++ stapling_negative_helper(Config, CACertsPath, ServerVariant, ExpectedError, #{ocsp_nonce => true}). ++ ++stapling_negative_helper(Config, CACertsPath, ServerVariant, ExpectedError, StaplingOpt) -> + PrivDir = proplists:get_value(priv_dir, Config), + CACertsFile = filename:join(PrivDir, CACertsPath), + GroupName = undefined, +@@ -274,7 +320,7 @@ stapling_negative_helper(Config, CACertsPath, ServerVariant, ExpectedError) -> + ClientOpts = ssl_test_lib:ssl_options([{verify, verify_peer}, + {server_name_indication, disable}, + {cacertfile, CACertsFile}, +- {stapling, #{ocsp_nonce => true}}], ++ {stapling, StaplingOpt}], + Config), + Client = ssl_test_lib:start_client_error([{node, ClientNode},{port, Port}, + {host, Hostname}, {from, self()}, +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-32147.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-32147.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-32147.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-32147.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,1434 @@ +From 6bb8816f19ff4259ef0aa0c93475840745afa40b Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Mon, 20 Apr 2026 19:44:17 +0200 +Subject: [PATCH] Merge branch + 'michal/ssh/fix-access-attributes-outside-root/OTP-20081' into maint-27 + +* michal/ssh/fix-access-attributes-outside-root/OTP-20081: + Add ssh keys for sftpd_SUITE + Add license header + Remove comments when installing ssh keys + Fix root escape vulnerability in SSH_FXP_FSETSTAT + +# Conflicts: +# lib/ssh/src/ssh_sftpd.erl +# lib/ssh/test/ssh_sftpd_SUITE.erl +# lib/ssh/test/ssh_test_lib.erl + +(cherry picked from commit ee89ffc95cf8855be030abb83be19e1e889d99e5) + +Debian note: this commit extends the -export list of +lib/ssh/test/ssh_test_lib.erl, and conflicts here. Upstream's version of that +list also exports server_host/1, server_port/1, server_pid/1, system_dir/1, +user_dir/1 and get_public_key_algorithms_with_valid_host_key/1,2, whose +definitions arrive in other, non-security commits that are not backported. +Exporting them makes the module fail to compile ("function server_host/1 +undefined") and aborts the whole ssh test run; nothing in this tree calls them. +The conflict is resolved by keeping only the entries whose functions exist: +event_logged/3 and remove_comment/1. +--- + lib/ssh/src/ssh_sftpd.erl | 62 ++++---- + lib/ssh/test/ssh_sftpd_SUITE.erl | 137 ++++++++++++++---- + lib/ssh/test/ssh_sftpd_SUITE_data/id_dsa | 40 +++-- + lib/ssh/test/ssh_sftpd_SUITE_data/id_dsa.pub | 20 +++ + lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa256 | 24 +++ + .../test/ssh_sftpd_SUITE_data/id_ecdsa256.pub | 20 +++ + lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa384 | 25 ++++ + .../test/ssh_sftpd_SUITE_data/id_ecdsa384.pub | 20 +++ + lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa521 | 26 ++++ + .../test/ssh_sftpd_SUITE_data/id_ecdsa521.pub | 20 +++ + lib/ssh/test/ssh_sftpd_SUITE_data/id_ed25519 | 26 ++++ + .../test/ssh_sftpd_SUITE_data/id_ed25519.pub | 20 +++ + lib/ssh/test/ssh_sftpd_SUITE_data/id_ed448 | 29 ++++ + .../test/ssh_sftpd_SUITE_data/id_ed448.pub | 20 +++ + lib/ssh/test/ssh_sftpd_SUITE_data/id_rsa | 46 ++++++ + lib/ssh/test/ssh_sftpd_SUITE_data/id_rsa.pub | 20 +++ + .../ssh_sftpd_SUITE_data/ssh_host_dsa_key | 19 +++ + .../ssh_sftpd_SUITE_data/ssh_host_dsa_key.pub | 19 +++ + .../ssh_host_ecdsa_key256 | 24 +++ + .../ssh_host_ecdsa_key256.pub | 20 +++ + .../ssh_host_ecdsa_key384 | 25 ++++ + .../ssh_host_ecdsa_key384.pub | 20 +++ + .../ssh_host_ecdsa_key521 | 26 ++++ + .../ssh_host_ecdsa_key521.pub | 20 +++ + .../ssh_sftpd_SUITE_data/ssh_host_ed25519_key | 26 ++++ + .../ssh_host_ed25519_key.pub | 20 +++ + .../ssh_sftpd_SUITE_data/ssh_host_ed448_key | 29 ++++ + .../ssh_host_ed448_key.pub | 20 +++ + .../ssh_sftpd_SUITE_data/ssh_host_rsa_key | 35 +++++ + .../ssh_sftpd_SUITE_data/ssh_host_rsa_key.pub | 24 +++ + lib/ssh/test/ssh_sftpd_SUITE_data/test.txt | 21 ++- + lib/ssh/test/ssh_test_lib.erl | 24 ++- + 32 files changed, 847 insertions(+), 80 deletions(-) + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/id_dsa.pub + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa256 + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa256.pub + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa384 + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa384.pub + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa521 + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa521.pub + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/id_ed25519 + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/id_ed25519.pub + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/id_ed448 + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/id_ed448.pub + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/id_rsa + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/id_rsa.pub + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key256 + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key256.pub + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key384 + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key384.pub + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key521 + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key521.pub + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed25519_key + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed25519_key.pub + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed448_key + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed448_key.pub + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_rsa_key + create mode 100644 lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_rsa_key.pub + +diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl +index e4a8af8e59..2f1982277d 100644 +--- a/lib/ssh/src/ssh_sftpd.erl ++++ b/lib/ssh/src/ssh_sftpd.erl +@@ -61,8 +61,8 @@ Specifies a channel process to handle an SFTP subsystem. + max_path, % integer > 0 - max length of path + options, % from the subsystem declaration + handles % list of open handles +- %% handle is either {, directory, {Path, unread|eof}} or +- %% {, file, {Path, IoDevice}} ++ %% handle is either {, directory, {AbsPath, unread|eof}} or ++ %% {, file, {AbsPath, IoDevice}} + }). + + %%==================================================================== +@@ -364,7 +364,7 @@ handle_op(?SSH_FXP_OPENDIR, ReqId, + "Not a directory"), + State1; + true when HandlesCnt < MaxHandles -> +- add_handle(State1, XF, ReqId, directory, {RelPath,unread}); ++ add_handle(State1, XF, ReqId, directory, {AbsPath,unread}); + true -> + ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_FAILURE, + "max_handles limit reached"), +@@ -375,11 +375,11 @@ handle_op(?SSH_FXP_READDIR, ReqId, + State) -> + XF = State#state.xf, + case get_handle(State#state.handles, BinHandle) of +- {_Handle, directory, {_RelPath, eof}} -> ++ {_Handle, directory, {_AbsPath, eof}} -> + ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_EOF), + State; +- {Handle, directory, {RelPath, Status}} -> +- read_dir(State, XF, ReqId, Handle, RelPath, Status); ++ {Handle, directory, {AbsPath, Status}} -> ++ read_dir(State, XF, ReqId, Handle, AbsPath, Status); + _ -> + ssh_xfer:xf_send_status(XF, ReqId, ?SSH_FX_INVALID_HANDLE), + State +@@ -415,7 +415,7 @@ handle_op(?SSH_FXP_READ, ReqId, <>, + State) -> + case get_handle(State#state.handles, BinHandle) of +- {_Handle, file, {_Path, IoDevice}} -> ++ {_Handle, file, {_AbsPath, IoDevice}} -> + read_file(ReqId, IoDevice, Offset, Len, State); + _ -> + ssh_xfer:xf_send_status(State#state.xf, ReqId, +@@ -426,7 +426,7 @@ handle_op(?SSH_FXP_WRITE, ReqId, + <>, State) -> + case get_handle(State#state.handles, BinHandle) of +- {_Handle, file, {_Path, IoDevice}} -> ++ {_Handle, file, {_AbsPath, IoDevice}} -> + write_file(ReqId, IoDevice, Offset, Data, State); + _ -> + ssh_xfer:xf_send_status(State#state.xf, ReqId, +@@ -469,8 +469,8 @@ handle_op(?SSH_FXP_FSETSTAT, ReqId, < + + case get_handle(Handles, BinHandle) of +- {_Handle, _Type, {Path,_}} -> +- {Status, State1} = set_stat(Attr, Path, State0), ++ {_Handle, _Type, {AbsPath,_}} -> ++ {Status, State1} = set_stat(Attr, AbsPath, State0), + send_status(Status, ReqId, State1); + _ -> + ssh_xfer:xf_send_status(State0#state.xf, ReqId, +@@ -576,8 +576,7 @@ get_handle(Handles, BinHandle) -> + + %%% read_dir/5: read directory, send names, and return new state + read_dir(State0 = #state{file_handler = FileMod, max_files = MaxLength, file_state = FS0}, +- XF = #ssh_xfer{cm = _CM, channel = _Channel, vsn = Vsn}, ReqId, Handle, RelPath, {cache, Files}) -> +- AbsPath = relate_file_name(RelPath, State0), ++ XF = #ssh_xfer{cm = _CM, channel = _Channel, vsn = Vsn}, ReqId, Handle, AbsPath, {cache, Files}) -> + if + length(Files) > MaxLength -> + {ToSend, NewCache} = lists:split(MaxLength, Files), +@@ -585,19 +584,18 @@ read_dir(State0 = #state{file_handler = FileMod, max_files = MaxLength, file_sta + ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs), + Handles = lists:keyreplace(Handle, 1, + State0#state.handles, +- {Handle, directory, {RelPath,{cache, NewCache}}}), ++ {Handle, directory, {AbsPath,{cache, NewCache}}}), + State0#state{handles = Handles, file_state = FS1}; + true -> + {NamesAndAttrs, FS1} = get_attrs(AbsPath, Files, FileMod, FS0, Vsn), + ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs), + Handles = lists:keyreplace(Handle, 1, + State0#state.handles, +- {Handle, directory, {RelPath,eof}}), ++ {Handle, directory, {AbsPath,eof}}), + State0#state{handles = Handles, file_state = FS1} + end; + read_dir(State0 = #state{file_handler = FileMod, max_files = MaxLength, file_state = FS0}, +- XF = #ssh_xfer{cm = _CM, channel = _Channel, vsn = Vsn}, ReqId, Handle, RelPath, _Status) -> +- AbsPath = relate_file_name(RelPath, State0), ++ XF = #ssh_xfer{cm = _CM, channel = _Channel, vsn = Vsn}, ReqId, Handle, AbsPath, _Status) -> + {Res, FS1} = FileMod:list_dir(AbsPath, FS0), + case Res of + {ok, Files} when MaxLength == 0 orelse MaxLength > length(Files) -> +@@ -605,7 +603,7 @@ read_dir(State0 = #state{file_handler = FileMod, max_files = MaxLength, file_sta + ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs), + Handles = lists:keyreplace(Handle, 1, + State0#state.handles, +- {Handle, directory, {RelPath,eof}}), ++ {Handle, directory, {AbsPath,eof}}), + State0#state{handles = Handles, file_state = FS2}; + {ok, Files} -> + {ToSend, Cache} = lists:split(MaxLength, Files), +@@ -613,7 +611,7 @@ read_dir(State0 = #state{file_handler = FileMod, max_files = MaxLength, file_sta + ssh_xfer:xf_send_names(XF, ReqId, NamesAndAttrs), + Handles = lists:keyreplace(Handle, 1, + State0#state.handles, +- {Handle, directory, {RelPath,{cache, Cache}}}), ++ {Handle, directory, {AbsPath,{cache, Cache}}}), + State0#state{handles = Handles, file_state = FS2}; + {error, Error} -> + State1 = State0#state{file_state = FS1}, +@@ -669,13 +667,13 @@ get_long_name(FileName, I) when is_record(I, file_info) -> + I#file_info.mode, I#file_info.uid, I#file_info.gid}). + + %%% get_attrs: get stat of each file and return +-get_attrs(RelPath, Files, FileMod, FS, Vsn) -> +- get_attrs(RelPath, Files, FileMod, FS, Vsn, []). ++get_attrs(AbsBase, Files, FileMod, FS, Vsn) -> ++ get_attrs(AbsBase, Files, FileMod, FS, Vsn, []). + +-get_attrs(_RelPath, [], _FileMod, FS, _Vsn, Acc) -> ++get_attrs(_AbsBase, [], _FileMod, FS, _Vsn, Acc) -> + {lists:reverse(Acc), FS}; +-get_attrs(RelPath, [F | Rest], FileMod, FS0, Vsn, Acc) -> +- Path = filename:absname(F, RelPath), ++get_attrs(AbsBase, [F | Rest], FileMod, FS0, Vsn, Acc) -> ++ Path = filename:absname(F, AbsBase), + case FileMod:read_link_info(Path, FS0) of + {{ok, Info}, FS1} -> + Name = if Vsn =< 3 -> +@@ -685,12 +683,12 @@ get_attrs(RelPath, [F | Rest], FileMod, FS0, Vsn, Acc) -> + F + end, + Attrs = ssh_sftp:info_to_attr(Info), +- get_attrs(RelPath, Rest, FileMod, FS1, Vsn, [{Name, Attrs} | Acc]); ++ get_attrs(AbsBase, Rest, FileMod, FS1, Vsn, [{Name, Attrs} | Acc]); + {{error, Msg}, FS1} when + Msg == enoent ; % The item has disappeared after reading the list of items to check + Msg == eacces -> % You are not allowed to read this + %% Skip this F and check the remaining Rest +- get_attrs(RelPath, Rest, FileMod, FS1, Vsn, Acc); ++ get_attrs(AbsBase, Rest, FileMod, FS1, Vsn, Acc); + {Error, FS1} -> + {Error, FS1} + end. +@@ -713,23 +711,25 @@ fstat(Vsn, ReqId, Data, State) when Vsn >= 4-> + + fstat(ReqId, BinHandle, State) -> + case get_handle(State#state.handles, BinHandle) of +- {_Handle, _Type, {Path, _}} -> +- stat(ReqId, Path, State, read_file_info); ++ {_Handle, _Type, {AbsPath, _}} -> ++ do_stat(ReqId, AbsPath, State, read_file_info); + _ -> + ssh_xfer:xf_send_status(State#state.xf, ReqId, + ?SSH_FX_INVALID_HANDLE), + State + end. + +-stat(ReqId, RelPath, State0=#state{file_handler=FileMod, +- file_state=FS0}, F) -> ++stat(ReqId, RelPath, State0, F) -> + AbsPath = relate_file_name(RelPath, State0), ++ do_stat(ReqId, AbsPath, State0, F). ++ ++do_stat(ReqId, AbsPath, State0=#state{file_handler=FileMod, file_state=FS0}, F) -> + XF = State0#state.xf, + {Res, FS1} = FileMod:F(AbsPath, FS0), + State1 = State0#state{file_state = FS1}, + case Res of + {ok, FileInfo} -> +- ssh_xfer:xf_send_attr(XF, ReqId, ++ ssh_xfer:xf_send_attr(XF, ReqId, + ssh_sftp:info_to_attr(FileInfo)), + State1; + {error, E} -> +@@ -839,7 +839,7 @@ do_open(ReqId, State0, Path, Flags) -> + State1 = State0#state{file_state = FS1}, + case Res of + {ok, IoDevice} -> +- add_handle(State1, State0#state.xf, ReqId, file, {Path,IoDevice}); ++ add_handle(State1, State0#state.xf, ReqId, file, {AbsPath,IoDevice}); + {error, Error} -> + ssh_xfer:xf_send_status(State1#state.xf, ReqId, + ssh_xfer:encode_erlang_status(Error)), +diff --git a/lib/ssh/test/ssh_sftpd_SUITE.erl b/lib/ssh/test/ssh_sftpd_SUITE.erl +index 989ff8427d..2c6655ef12 100644 +--- a/lib/ssh/test/ssh_sftpd_SUITE.erl ++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl +@@ -54,7 +54,8 @@ + ver3_open_flags/1, + ver3_rename/1, + ver6_basic/1, +- write_file/1 ++ write_file/1, ++ access_attributes_outside_root/1 + ]). + + -include_lib("common_test/include/ct.hrl"). +@@ -103,7 +104,8 @@ all() -> + root_with_cwd, + relative_path, + open_file_dir_v5, +- open_file_dir_v6]. ++ open_file_dir_v6, ++ access_attributes_outside_root]. + + groups() -> + []. +@@ -139,6 +141,7 @@ end_per_group(_GroupName, Config) -> + %%-------------------------------------------------------------------- + + init_per_testcase(TestCase, Config) -> ++ {OsFamily, _} = os:type(), + ssh:start(), + prep(Config), + PrivDir = proplists:get_value(priv_dir, Config), +@@ -149,7 +152,7 @@ init_per_testcase(TestCase, Config) -> + {user_dir, PrivDir}, + {user_passwords,[{?USER, ?PASSWD}]}, + {pwdfun, fun(_,_) -> true end}], +- {ok, Sftpd} = case TestCase of ++ Result = case TestCase of + ver6_basic -> + SubSystems = [ssh_sftpd:subsystem_spec([{sftpd_vsn, 6}])], + ssh:daemon(0, [{subsystems, SubSystems}|Options]); +@@ -163,6 +166,14 @@ init_per_testcase(TestCase, Config) -> + SubSystems = [ssh_sftpd:subsystem_spec([{root, RootDir}, + {cwd, CWD}])], + ssh:daemon(0, [{subsystems, SubSystems}|Options]); ++ access_attributes_outside_root when OsFamily =:= win32 -> ++ {skip, "Not implemented on windows"}; ++ access_attributes_outside_root -> ++ Rand = integer_to_list(rand:uniform(1000000)), ++ RootDir = filename:join("/tmp", Rand), ++ ok = file:make_dir(RootDir), ++ SubSystems = [ssh_sftpd:subsystem_spec([{root, RootDir}])], ++ ssh:daemon(0, [{subsystems, SubSystems}|Options]); + root_with_cwd -> + RootDir = filename:join(PrivDir, root_with_cwd), + CWD = filename:join(RootDir, home), +@@ -184,42 +195,59 @@ init_per_testcase(TestCase, Config) -> + {max_path, ?MAX_PATH}])], + ssh:daemon(0, [{subsystems, SubSystems}|Options]) + end, +- +- Port = ssh_test_lib:daemon_port(Sftpd), + +- Cm = ssh_test_lib:connect(Port, +- [{user_dir, ClientUserDir}, +- {user, ?USER}, {password, ?PASSWD}, +- {user_interaction, false}, +- {silently_accept_hosts, true}]), +- {ok, Channel} = +- ssh_connection:session_channel(Cm, ?XFER_WINDOW_SIZE, +- ?XFER_PACKET_SIZE, ?SSH_TIMEOUT), +- +- success = ssh_connection:subsystem(Cm, Channel, "sftp", ?SSH_TIMEOUT), ++ case Result of ++ {ok, Sftpd} -> ++ Port = ssh_test_lib:daemon_port(Sftpd), + +- ProtocolVer = case atom_to_list(TestCase) of +- "ver3_" ++ _ -> +- 3; +- _ -> +- ?SSH_SFTP_PROTOCOL_VERSION +- end, ++ Cm = ssh_test_lib:connect(Port, ++ [{user_dir, ClientUserDir}, ++ {user, ?USER}, {password, ?PASSWD}, ++ {user_interaction, false}, ++ {silently_accept_hosts, true}]), ++ {ok, Channel} = ++ ssh_connection:session_channel(Cm, ?XFER_WINDOW_SIZE, ++ ?XFER_PACKET_SIZE, ?SSH_TIMEOUT), + +- Data = <> , ++ success = ssh_connection:subsystem(Cm, Channel, "sftp", ?SSH_TIMEOUT), + +- Size = 1 + size(Data), ++ ProtocolVer = case atom_to_list(TestCase) of ++ "ver3_" ++ _ -> ++ 3; ++ _ -> ++ ?SSH_SFTP_PROTOCOL_VERSION ++ end, ++ ++ Data = <> , + +- ssh_connection:send(Cm, Channel, << ?UINT32(Size), +- ?SSH_FXP_INIT, Data/binary >>), ++ Size = 1 + size(Data), + +- {ok, <>, _} +- = reply(Cm, Channel), ++ ssh_connection:send(Cm, Channel, << ?UINT32(Size), ++ ?SSH_FXP_INIT, Data/binary >>), + +- ct:log("Client: ~p Server ~p~n", [ProtocolVer, Version]), ++ {ok, <>, _} ++ = reply(Cm, Channel), + +- [{sftp, {Cm, Channel}}, {sftpd, Sftpd }| Config]. ++ ct:log("Client: ~p Server ~p~n", [ProtocolVer, Version]), + ++ [{sftp, {Cm, Channel}}, {sftpd, Sftpd }| Config]; ++ Other -> ++ Other ++ end. ++ ++end_per_testcase(access_attributes_outside_root, Config) -> ++ Sftpd = proplists:get_value(sftpd, Config), ++ {ok, DaemonInfo} = ssh:daemon_info(Sftpd), ++ ssh_cleanup(Config), ++ DaemonOpts = proplists:get_value(options, DaemonInfo), ++ Subsystems = proplists:get_value(subsystems, DaemonOpts), ++ {_, {_, SftpdOpts}} = lists:keyfind("sftp", 1, Subsystems), ++ RootDir = proplists:get_value(root, SftpdOpts), ++ file:del_dir_r(RootDir); + end_per_testcase(_TestCase, Config) -> ++ ssh_cleanup(Config). ++ ++ssh_cleanup(Config) -> + try + ssh:stop_daemon(proplists:get_value(sftpd, Config)) + catch +@@ -818,6 +846,53 @@ open_file_dir_v6(Config) when is_list(Config) -> + ?ACE4_READ_DATA bor ?ACE4_READ_ATTRIBUTES, + ?SSH_FXF_OPEN_EXISTING). + ++%%-------------------------------------------------------------------- ++access_attributes_outside_root(Config) when is_list(Config) -> ++ Sftpd = proplists:get_value(sftpd, Config), ++ {ok, DaemonInfo} = ssh:daemon_info(Sftpd), ++ DaemonOpts = proplists:get_value(options, DaemonInfo), ++ Subsystems = proplists:get_value(subsystems, DaemonOpts), ++ {_, {_, SftpdOpts}} = lists:keyfind("sftp", 1, Subsystems), ++ RootDir = proplists:get_value(root, SftpdOpts), ++ ++ TargetName = "target-" ++ filename:basename(RootDir) ++ ".txt", ++ InsideRootFile = filename:join([RootDir, "tmp", TargetName]), ++ ok = file:make_dir(filename:dirname(InsideRootFile)), ++ ok = file:write_file(InsideRootFile, <<"inside root">>), ++ {ok, InsideRootFileInfo} = file:read_file_info(InsideRootFile), ++ InsideRootFileMode = InsideRootFileInfo#file_info.mode, ++ ++ OutsideRootFile = filename:join("/tmp", TargetName), ++ ok = file:write_file(OutsideRootFile, <<"outside root">>), ++ try ++ {ok, OutsideRootFileInfo} = file:read_file_info(OutsideRootFile), ++ OutsideRootFileMode = OutsideRootFileInfo#file_info.mode, ++ ++ {Cm, Channel} = proplists:get_value(sftp, Config), ++ ReqId0 = 0, ++ {ok, <>, _} = ++ open_file(OutsideRootFile, Cm, Channel, ReqId0, ++ ?ACE4_READ_DATA bor ?ACE4_WRITE_ATTRIBUTES, ++ ?SSH_FXF_OPEN_EXISTING), ++ ++ Attrs = [?uint32(?SSH_FILEXFER_ATTR_PERMISSIONS), ?byte(?SSH_FILEXFER_TYPE_REGULAR), ++ ?uint32(not_default_permissions())], ++ ++ ReqId1 = 1, ++ {ok, <>, _} = ++ set_attributes_open_file(Handle, Attrs, Cm, Channel, ReqId1), ++ ++ {ok, NewOutsideRootFileInfo} = file:read_file_info(OutsideRootFile), ++ NewOutsideRootFileMode = NewOutsideRootFileInfo#file_info.mode, ++ ?assertEqual(OutsideRootFileMode, NewOutsideRootFileMode), ++ ++ {ok, NewInsideRootFileInfo} = file:read_file_info(InsideRootFile), ++ NewInsideRootFileMode = NewInsideRootFileInfo#file_info.mode, ++ ?assertNotEqual(InsideRootFileMode, NewInsideRootFileMode) ++ after ++ file:delete(OutsideRootFile) ++ end. ++ + %%-------------------------------------------------------------------- + %% Internal functions ------------------------------------------------ + %%-------------------------------------------------------------------- +@@ -832,7 +907,9 @@ prep(Config) -> + %% Initial config + DataDir = proplists:get_value(data_dir, Config), + FileName = filename:join(DataDir, "test.txt"), +- file:copy(FileName, TestFile), ++ {ok, Data0} = file:read_file(FileName), ++ Data = ssh_test_lib:remove_comment(Data0), ++ ok = file:write_file(TestFile, string:chomp(Data)), + Mode = 8#00400 bor 8#00200 bor 8#00040, % read & write owner, read group + {ok, FileInfo} = file:read_file_info(TestFile), + ok = file:write_file_info(TestFile, +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/id_dsa b/lib/ssh/test/ssh_sftpd_SUITE_data/id_dsa +index d306f8b26e..60123777b8 100644 +--- a/lib/ssh/test/ssh_sftpd_SUITE_data/id_dsa ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/id_dsa +@@ -1,13 +1,31 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 2012-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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% + -----BEGIN DSA PRIVATE KEY----- +-MIIBvAIBAAKBgQDfi2flSTZZofwT4yQT0NikX/LGNT7UPeB/XEWe/xovEYCElfaQ +-APFixXvEgXwoojmZ5kiQRKzLM39wBP0jPERLbnZXfOOD0PDnw0haMh7dD7XKVMod +-/EigVgHf/qBdM2M8yz1s/rRF7n1UpLSypziKjkzCm7JoSQ2zbWIPdmBIXwIVAMgP +-kpr7Sq3O7sHdb8D601DRjoExAoGAMOQxDfB2Fd8ouz6G96f/UOzRMI/Kdv8kYYKW +-JIGY+pRYrLPyYzUeJznwZreOJgrczAX+luHnKFWJ2Dnk5CyeXk67Wsr7pJ/4MBMD +-OKeIS0S8qoSBN8+Krp79fgA+yS3IfqbkJLtLu4EBaCX4mKQIX4++k44d4U5lc8pt +-+9hlEI8CgYEAznKxx9kyC6bVo7LUYKaGhofRFt0SYFc5PVmT2VUGRs1R6+6DPD+e +-uEO6IhFct7JFSRbP9p0JD4Uk+3zlZF+XX6b2PsZkeV8f/02xlNGUSmEzCSiNg1AX +-Cy/WusYhul0MncWCHMcOZB5rIvU/aP5EJJtn3xrRaz6u0SThF6AnT34CFQC63czE +-ZU8w8Q+H7z0j+a+70x2iAw== ++MIIBuwIBAAKBgQDIywHurUpOq6kZuMn+XlRzR4hAxF6qwSkuEqkV7iHnLQ0kIwf3 ++uAmjFDhuEsQ8653SLxGVvTNp+KFFgDXiLqgM7TPUwDnpbvzEZHPAU+/zPt4sdY2D ++txBfJwT2SFlK6HPOxOcxdDuD+/a59sh8hk/YVOU7ZTcBVsVG8Got4UcF5QIVAPGd ++CPDQKSTlPiM9OwBB1+9p11k5AoGARLxw4l17mET9cU0uf4Ppe5nsCbODJv44ZrSs ++picvypGVLrLcN5KWbm3vjRFCQ5LFunAG3FwLC2Sh0CH6TemoIfRPsRHR7wvpBGdr ++c693UlMOis/mcmvNMQAzuQNW9WrxdzsvWR/r5s6NEHWqKUJGXSPi2d+Ijq/mCOmI ++hzLzyiACgYEAsTRcHZqZlamr0PM7jKt2edCpcd8rEFGtWuescebc6Ga5JGSv7Ue4 ++cdYKpAjT10Mns1WYaU9t6ZR+6ARP7DkzzDmS1elwkRu21T+b81PmeZwaEJxgqr+C ++ROQVHgzpqMqEx8ic3c/juxZpRrCAlRCjCWSJLDMobBQvtfyG0qsleNgCFEjA7wTC ++sQCY/I35vb6GUJn9tEdP + -----END DSA PRIVATE KEY----- +- +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/id_dsa.pub b/lib/ssh/test/ssh_sftpd_SUITE_data/id_dsa.pub +new file mode 100644 +index 0000000000..bbb9527c41 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/id_dsa.pub +@@ -0,0 +1,20 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++ssh-dss AAAAB3NzaC1kc3MAAACBAMjLAe6tSk6rqRm4yf5eVHNHiEDEXqrBKS4SqRXuIectDSQjB/e4CaMUOG4SxDzrndIvEZW9M2n4oUWANeIuqAztM9TAOelu/MRkc8BT7/M+3ix1jYO3EF8nBPZIWUroc87E5zF0O4P79rn2yHyGT9hU5TtlNwFWxUbwai3hRwXlAAAAFQDxnQjw0Ckk5T4jPTsAQdfvaddZOQAAAIBEvHDiXXuYRP1xTS5/g+l7mewJs4Mm/jhmtKymJy/KkZUustw3kpZube+NEUJDksW6cAbcXAsLZKHQIfpN6agh9E+xEdHvC+kEZ2tzr3dSUw6Kz+Zya80xADO5A1b1avF3Oy9ZH+vmzo0QdaopQkZdI+LZ34iOr+YI6YiHMvPKIAAAAIEAsTRcHZqZlamr0PM7jKt2edCpcd8rEFGtWuescebc6Ga5JGSv7Ue4cdYKpAjT10Mns1WYaU9t6ZR+6ARP7DkzzDmS1elwkRu21T+b81PmeZwaEJxgqr+CROQVHgzpqMqEx8ic3c/juxZpRrCAlRCjCWSJLDMobBQvtfyG0qsleNg= uabhnil@elxadlj3q32 +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa256 b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa256 +new file mode 100644 +index 0000000000..0a8868e9e5 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa256 +@@ -0,0 +1,24 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++-----BEGIN EC PRIVATE KEY----- ++MHcCAQEEIJfCaBKIIKhjbJl5F8BedqlXOQYDX5ba9Skypllmx/w+oAoGCCqGSM49 ++AwEHoUQDQgAE49RbK2xQ/19ji3uDPM7uT4692LbwWF1TiaA9vUuebMGazoW/98br ++N9xZu0L1AWwtEjs3kmJDTB7eJEGXnjUAcQ== ++-----END EC PRIVATE KEY----- +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa256.pub b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa256.pub +new file mode 100644 +index 0000000000..ef6f410bc6 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa256.pub +@@ -0,0 +1,20 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOPUWytsUP9fY4t7gzzO7k+Ovdi28FhdU4mgPb1LnmzBms6Fv/fG6zfcWbtC9QFsLRI7N5JiQ0we3iRBl541AHE= uabhnil@elxadlj3q32 +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa384 b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa384 +new file mode 100644 +index 0000000000..5a9ef32b1c +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa384 +@@ -0,0 +1,25 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++-----BEGIN EC PRIVATE KEY----- ++MIGkAgEBBDCYXb6OSAZyXRfLXOtMo43za197Hdc/T0YKjgQQjwDt6rlRwqTh7v7S ++PV2kXwNGdWigBwYFK4EEACKhZANiAARN2khlJUOOIiwsWHEALwDieeZR96qL4pUd ++ci7aeGaczdUK5jOA9D9zmBZtSYTfO8Cr7ekVghDlcWAIJ/BXcswgQwSEQ6wyfaTF ++8FYfyr4l3u9IirsnyaFzeIgeoNis8Gw= ++-----END EC PRIVATE KEY----- +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa384.pub b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa384.pub +new file mode 100644 +index 0000000000..aa3e05db63 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa384.pub +@@ -0,0 +1,20 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBE3aSGUlQ44iLCxYcQAvAOJ55lH3qovilR1yLtp4ZpzN1QrmM4D0P3OYFm1JhN87wKvt6RWCEOVxYAgn8FdyzCBDBIRDrDJ9pMXwVh/KviXe70iKuyfJoXN4iB6g2KzwbA== uabhnil@elxadlj3q32 +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa521 b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa521 +new file mode 100644 +index 0000000000..21f489cd40 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa521 +@@ -0,0 +1,26 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++-----BEGIN EC PRIVATE KEY----- ++MIHbAgEBBEFMadoz4ckEcClfqXa2tiUuYkJdDfwq+/iFQcpt8ESuEd26IY/vm47Q ++9UzbPkO4ou8xkNsQ3WvCRQBBWtn5O2kUU6AHBgUrgQQAI6GBiQOBhgAEAde5BRu5 ++01/jS0jRk212xsb2DxPrxNpgp6IMCV8TA4Eps+8bSqHB091nLiBcP422HXYfuCd7 ++XDjSs8ihcmhp0hCRASLqZR9EzW9W/SOt876May1Huj5X+WSO6RLe7vPn9vmf7kHf ++pip6m7M7qp2qGgQ3q2vRwS2K/O6156ohiOlmuuFs ++-----END EC PRIVATE KEY----- +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa521.pub b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa521.pub +new file mode 100644 +index 0000000000..77a0da2b39 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ecdsa521.pub +@@ -0,0 +1,20 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAHXuQUbudNf40tI0ZNtdsbG9g8T68TaYKeiDAlfEwOBKbPvG0qhwdPdZy4gXD+Nth12H7gne1w40rPIoXJoadIQkQEi6mUfRM1vVv0jrfO+jGstR7o+V/lkjukS3u7z5/b5n+5B36YqepuzO6qdqhoEN6tr0cEtivzuteeqIYjpZrrhbA== uabhnil@elxadlj3q32 +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/id_ed25519 b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ed25519 +new file mode 100644 +index 0000000000..291ee8dd0c +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ed25519 +@@ -0,0 +1,26 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++-----BEGIN OPENSSH PRIVATE KEY----- ++b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW ++QyNTUxOQAAACDm9P8/gC0IOKmwHLSvkmEtS2Xx0RRqUDqC6wY6UgDVnwAAAJg3+6xpN/us ++aQAAAAtzc2gtZWQyNTUxOQAAACDm9P8/gC0IOKmwHLSvkmEtS2Xx0RRqUDqC6wY6UgDVnw ++AAAEBzC/Z2WGJhZ3l3tIBnUc6DCbp+lXY2yc2RRpWQTdf8sub0/z+ALQg4qbActK+SYS1L ++ZfHRFGpQOoLrBjpSANWfAAAAE3VhYmhuaWxAZWx4YWRsajNxMzIBAg== ++-----END OPENSSH PRIVATE KEY----- +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/id_ed25519.pub b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ed25519.pub +new file mode 100644 +index 0000000000..15a4864504 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ed25519.pub +@@ -0,0 +1,20 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb0/z+ALQg4qbActK+SYS1LZfHRFGpQOoLrBjpSANWf uabhnil@elxadlj3q32 +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/id_ed448 b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ed448 +new file mode 100644 +index 0000000000..2f765b3011 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ed448 +@@ -0,0 +1,29 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++-----BEGIN OPENSSH PRIVATE KEY----- ++b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz ++c2gtZWQ0NDgAAAA53OqeePNaG/NJmoMbELhskKrAHNhLZ6AQm1WjbpMoseNl/OFh ++1xznExpUPqTLX36fHYsAaWRHABQAAAAA0AAAEREAABERAAAACXNzaC1lZDQ0OAAA ++ADnc6p5481ob80magxsQuGyQqsAc2EtnoBCbVaNukyix42X84WHXHOcTGlQ+pMtf ++fp8diwBpZEcAFAAAAAByzSPST3FCdOdENDI3uTKQ9RH2Ql+Y5kRZ/yA+iYUIP/32 ++BQBVOrwOBc0CGEvbicTM1n4YeVEmfrMo3OqeePNaG/NJmoMbELhskKrAHNhLZ6AQ ++m1WjbpMoseNl/OFh1xznExpUPqTLX36fHYsAaWRHABQAAAAAAAECAwQ= ++-----END OPENSSH PRIVATE KEY----- ++ +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/id_ed448.pub b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ed448.pub +new file mode 100644 +index 0000000000..7431b7b346 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/id_ed448.pub +@@ -0,0 +1,20 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++ssh-ed448 AAAACXNzaC1lZDQ0OAAAADnc6p5481ob80magxsQuGyQqsAc2EtnoBCbVaNukyix42X84WHXHOcTGlQ+pMtffp8diwBpZEcAFAA= uabhnil@elxadlj3q32 +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/id_rsa b/lib/ssh/test/ssh_sftpd_SUITE_data/id_rsa +new file mode 100644 +index 0000000000..b4f6920395 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/id_rsa +@@ -0,0 +1,46 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++-----BEGIN RSA PRIVATE KEY----- ++MIIEpAIBAAKCAQEAztjiyj2tdfkji0fewWS0kABg0IABgG20NvL1PnHJLr98we7w ++W7f3j27EGjW/ApuycsWXXKi0L82q8uDicoHHb3JI2JkT70oi0yG1Dx/zwPN+dkA7 ++LBT1J3UK2hJTFPhp855CwY/ss9xpBsd1Fv3zuHifEqNGljeg1PjmQ3pNhxA/M0aZ ++cLnfIUdZ5Hr+t+4es3zaWo4tLBKmZu6BkVGQKPGXeMkIAMtJlG24l7qKDRkR5TYA ++ZT7P8Vn7hnuFuCNbrJSm686GawBxTQXom23dg9UcWxoHB7UiHFoR6j0bQAX+4R7b ++IwculRDcvzrgCu6u06oFILwY7MlsxpX9hGTl1wIDAQABAoIBAFeP6pmQeICrYceR ++OhQGLIWVE2bP+VLDnflw6i5v/qlieE6kdm1tOEgorK0nuV9CR81cJdIcvIJL/yTn ++3BR7KdDcwUenrY+rg4h7CWmIrigtK4ilciccDBeS7XAZN8B11GxDv6Cu65XMJU2w ++W7nK8URTE4vRQI1QqS3e26MPAAi/LVOt3ZPI6zg/GHEwnq0IVSQAOndLBr/IWZk5 ++SANrkfwX8WS7/UxZgDptT9dyUQ5Pnj5mieTlIvBwyczdhZ7RDa8HdCSHW3xF83V1 ++A0pkn6+TRojumYyr4RrPQj6htE64Hgx9w1Dv/UINjPXl5mGlbxQHMWGzlqD/qpyI ++wg7RakECgYEA+9ARZpHfEFz+EEFi8l9J+BtJDo00WaKCOZHh5UJ8W+NreqSd8nSx ++5u6wYwMJjRX2Hwv+FBEhxGbo1+ff6p++cYmiSlDtN2XRCDkBWvvGlxu55BDULrhx ++f8lqaV3XGmOy2rQusp8hiHmkmPJCSVj3oJqQnbqJ2zahXAx1rTPwHqECgYEA0kln ++4h+ZkZ+aldOMGF0d0txTcTqZvsSVKiFTSD9of/fiSDqb6xtLT2+ys6FZoFL9lyK8 ++gtqH642CDQ+3WT6Nmn4kMF5HNVpEuCeRDeRhiquWeKaAQDyvZ5ym1+Cn3GhsO7Di ++d2LJKV5hOoN77loVY5nwnUVIJ0h+WLf0T7DTCXcCgYEAiNT7X50MdTvS4splFgcp ++jqRlAn9AXySrVtUqxwVlxhjCIpapLUK0GSTCvEq+OeghIaXGnujgTHUPOaNKTZgY ++SGHdyjxHar7s42b2kZYWx63NSVzLr8eSBTpRlIflhvV+DtGyPmWyNxLCmkmqM2kg ++xii3RL5EgtYgwIAUwdVjOYECgYBRPlsMWfkS8f7fc+PkZdVn6gey71kHAxw+MrHi ++b90H09Vw4nPq2Zi3EAiSrfvanTWsdpcuVw+8See89B16NViwH5wLs+D/E+kI3QCF ++xX6J/NEdu/ZA2zFJbpRnQzyXQyDNzwEv7tKZUQVvfe0boWIyIP99Q48k3jUyQZ/6 ++Se6+8QKBgQCXl8H2K3CsZxoujKLb2qoEOPbxJQ2hxoMTS5XuQECReIVsNuptWrur ++DF8WJi/B6AqwRX1P3l56RNwqB1yDBqv0QVLpU7vU/FmWqLWTn0r3AvM74qftvfAE ++oa31wcYoCqPJoKgCG7TThLhNt2v5hL7sVgZNO0ueAiHhJbFLaf7ceg== ++-----END RSA PRIVATE KEY----- +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/id_rsa.pub b/lib/ssh/test/ssh_sftpd_SUITE_data/id_rsa.pub +new file mode 100644 +index 0000000000..38dcba9a14 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/id_rsa.pub +@@ -0,0 +1,20 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDO2OLKPa11+SOLR97BZLSQAGDQgAGAbbQ28vU+cckuv3zB7vBbt/ePbsQaNb8Cm7JyxZdcqLQvzary4OJygcdvckjYmRPvSiLTIbUPH/PA8352QDssFPUndQraElMU+GnznkLBj+yz3GkGx3UW/fO4eJ8So0aWN6DU+OZDek2HED8zRplwud8hR1nkev637h6zfNpaji0sEqZm7oGRUZAo8Zd4yQgAy0mUbbiXuooNGRHlNgBlPs/xWfuGe4W4I1uslKbrzoZrAHFNBeibbd2D1RxbGgcHtSIcWhHqPRtABf7hHtsjBy6VENy/OuAK7q7TqgUgvBjsyWzGlf2EZOXX uabhnil@elxadlj3q32 +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_dsa_key b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_dsa_key +index 51ab6fbd88..d9b1187dad 100644 +--- a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_dsa_key ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_dsa_key +@@ -1,3 +1,22 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 2012-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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% + -----BEGIN DSA PRIVATE KEY----- + MIIBuwIBAAKBgQCClaHzE2ul0gKSUxah5W0W8UiJLy4hXngKEqpaUq9SSdVdY2LK + wVfKH1gt5iuaf1FfzOhsIC9G/GLnjYttXZc92cv/Gfe3gR+s0ni2++MX+T++mE/Q +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_dsa_key.pub b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_dsa_key.pub +index 4dbb1305b0..93fd02358f 100644 +--- a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_dsa_key.pub ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_dsa_key.pub +@@ -1,3 +1,22 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 2012-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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% + ---- BEGIN SSH2 PUBLIC KEY ---- + AAAAB3NzaC1kc3MAAACBAIKVofMTa6XSApJTFqHlbRbxSIkvLiFeeAoSqlpSr1JJ1V1j + YsrBV8ofWC3mK5p/UV/M6GwgL0b8YueNi21dlz3Zy/8Z97eBH6zSeLb74xf5P76YT9B2 +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key256 b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key256 +new file mode 100644 +index 0000000000..7507f57681 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key256 +@@ -0,0 +1,24 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++-----BEGIN EC PRIVATE KEY----- ++MHcCAQEEIMe4MDoit0t8RzSVPwkCBemQ9fhXL+xnTSAWISw8HNCioAoGCCqGSM49 ++AwEHoUQDQgAEo2q7U3P6r0W5WGOLtM78UQtofM9UalEhiZeDdiyylsR/RR17Op0s ++VPGSADLmzzgcucLEKy17j2S+oz42VUJy5A== ++-----END EC PRIVATE KEY----- +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key256.pub b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key256.pub +new file mode 100644 +index 0000000000..edbd0ecdab +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key256.pub +@@ -0,0 +1,20 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKNqu1Nz+q9FuVhji7TO/FELaHzPVGpRIYmXg3YsspbEf0UdezqdLFTxkgAy5s84HLnCxCste49kvqM+NlVCcuQ= uabhnil@elxadlj3q32 +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key384 b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key384 +new file mode 100644 +index 0000000000..c316ff4b2f +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key384 +@@ -0,0 +1,25 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++-----BEGIN EC PRIVATE KEY----- ++MIGkAgEBBDArxbDfh3p1okrD9wQw6jJ4d4DdlBPD5GqXE8bIeRJiK41Sh40LgvPw ++mkqEDSXK++CgBwYFK4EEACKhZANiAAScl43Ih2lWTDKrSox5ve5uiTXil4smsup3 ++CfS1XPjKxgBAmlfBim8izbdrT0BFdQzz2joduNMtpt61wO4rGs6jm0UP7Kim9PC7 ++Hneb/99fIYopdMH5NMnk60zGO1uZ2vc= ++-----END EC PRIVATE KEY----- +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key384.pub b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key384.pub +new file mode 100644 +index 0000000000..56ba3c369b +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key384.pub +@@ -0,0 +1,20 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBJyXjciHaVZMMqtKjHm97m6JNeKXiyay6ncJ9LVc+MrGAECaV8GKbyLNt2tPQEV1DPPaOh240y2m3rXA7isazqObRQ/sqKb08Lsed5v/318hiil0wfk0yeTrTMY7W5na9w== uabhnil@elxadlj3q32 +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key521 b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key521 +new file mode 100644 +index 0000000000..b1ea9615c3 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key521 +@@ -0,0 +1,26 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++-----BEGIN EC PRIVATE KEY----- ++MIHcAgEBBEIB8O1BFkl2HQjQLRLonEZ97da/h39DMa9/0/hvPZWAI8gUPEQcHxRx ++U7b09p3Zh+EBbMFq8+1ae9ds+ZTxE4WFSvKgBwYFK4EEACOhgYkDgYYABAAlWVjq ++Bzg7Wt4gE6UNb1lRE2cnlmH2L/A5uo6qZRx5lPnSKOxEhxSb/Oay1+9d6KRdrh6/ ++vlhd9SHDBhLcAPDvWgBnJIEj92Q3pXX4JtoitL0yl+SvvU+vUh966mzHShHzj8p5 ++ccOgPkPNoA70yrpGzkIhPezpZOQdCaOXj/jFqNCTDg== ++-----END EC PRIVATE KEY----- +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key521.pub b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key521.pub +new file mode 100644 +index 0000000000..545766ecda +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ecdsa_key521.pub +@@ -0,0 +1,20 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAAlWVjqBzg7Wt4gE6UNb1lRE2cnlmH2L/A5uo6qZRx5lPnSKOxEhxSb/Oay1+9d6KRdrh6/vlhd9SHDBhLcAPDvWgBnJIEj92Q3pXX4JtoitL0yl+SvvU+vUh966mzHShHzj8p5ccOgPkPNoA70yrpGzkIhPezpZOQdCaOXj/jFqNCTDg== uabhnil@elxadlj3q32 +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed25519_key b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed25519_key +new file mode 100644 +index 0000000000..e4b27e5b4b +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed25519_key +@@ -0,0 +1,26 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++-----BEGIN OPENSSH PRIVATE KEY----- ++b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW ++QyNTUxOQAAACBJSOuiYGWaO9lye8Bgafod1kw8P6cV3Xb2qJgCB6yJfQAAAJi+h4O7voeD ++uwAAAAtzc2gtZWQyNTUxOQAAACBJSOuiYGWaO9lye8Bgafod1kw8P6cV3Xb2qJgCB6yJfQ ++AAAEBaOcJfGPNemKc1wPHTCmM4Kwvh6dZ0CqY14UT361UnN0lI66JgZZo72XJ7wGBp+h3W ++TDw/pxXddvaomAIHrIl9AAAAE3VhYmhuaWxAZWx4YWRsajNxMzIBAg== ++-----END OPENSSH PRIVATE KEY----- +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed25519_key.pub b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed25519_key.pub +new file mode 100644 +index 0000000000..fb17383c7f +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed25519_key.pub +@@ -0,0 +1,20 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIElI66JgZZo72XJ7wGBp+h3WTDw/pxXddvaomAIHrIl9 uabhnil@elxadlj3q32 +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed448_key b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed448_key +new file mode 100644 +index 0000000000..9780dba8af +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed448_key +@@ -0,0 +1,29 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++-----BEGIN OPENSSH PRIVATE KEY----- ++b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz ++c2gtZWQ0NDgAAAA5X9dEm1m0Yf0s54fsYWrUah2hNCSFpw4fig6nXYDpZ3jt8SR2 ++m0bHBhvWeD3x5Q9s0foavq/oJWGAAAAA0AAAEREAABERAAAACXNzaC1lZDQ0OAAA ++ADlf10SbWbRh/Sznh+xhatRqHaE0JIWnDh+KDqddgOlneO3xJHabRscGG9Z4PfHl ++D2zR+hq+r+glYYAAAABybIKlYsuAjRDWMr6JyFE+v2ySnzTd+oyfY8mWDvbjSKNS ++jIo/zC8ETjmj/FuUSS+PAy51SaIAmPlbX9dEm1m0Yf0s54fsYWrUah2hNCSFpw4f ++ig6nXYDpZ3jt8SR2m0bHBhvWeD3x5Q9s0foavq/oJWGAAAAAAAECAwQ= ++-----END OPENSSH PRIVATE KEY----- ++ +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed448_key.pub b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed448_key.pub +new file mode 100644 +index 0000000000..c01268415e +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_ed448_key.pub +@@ -0,0 +1,20 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++ssh-ed448 AAAACXNzaC1lZDQ0OAAAADlf10SbWbRh/Sznh+xhatRqHaE0JIWnDh+KDqddgOlneO3xJHabRscGG9Z4PfHlD2zR+hq+r+glYYA= +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_rsa_key b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_rsa_key +new file mode 100644 +index 0000000000..a66b591b80 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_rsa_key +@@ -0,0 +1,35 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++-----BEGIN RSA PRIVATE KEY----- ++MIICXQIBAAKBgQDCZX+4FBDwZIh9y/Uxee1VJnEXlowpz2yDKwj8semM4q843337 ++zbNfxHmladB1lpz2NqyxI175xMIJuDxogyZdsOxGnFAzAnthR4dqL/RWRWzjaxSB ++6IAO9SPYVVlrpZ+1hsjLW79fwXK/yc8VdhRuWTeQiRgYY2ek8+OKbOqz4QIDAQAB ++AoGANmvJzJO5hkLuvyDZHKfAnGTtpifcR1wtSa9DjdKUyn8vhKF0mIimnbnYQEmW ++NUUb3gXCZLi9PvkpRSVRrASDOZwcjoU/Kvww163vBUVb2cOZfFhyn6o2Sk88Tt++ ++udH3hdjpf9i7jTtUkUe+QYPsia+wgvvrmn4QrahLAH86+kECQQDx5gFeXTME3cnW ++WMpFz3PPumduzjqgqMMWEccX4FtQkMX/gyGa5UC7OHFyh0N/gSWvPbRHa8A6YgIt ++n8DO+fh5AkEAzbqX4DOn8NY6xJIi42q7l/2jIA0RkB6P7YugW5NblhqBZ0XDnpA5 ++sMt+rz+K07u9XZtxgh1xi7mNfwY6lEAMqQJBAJBEauCKmRj35Z6OyeQku59SPsnY +++SJEREVvSNw2lH9SOKQQ4wPsYlTGbvKtNVZgAcen91L5MmYfeckYE/fdIZECQQCt ++64zxsTnM1I8iFxj/gP/OYlJBikrKt8udWmjaghzvLMEw+T2DExJyb9ZNeT53+UMB ++m6O+B/4xzU/djvp+0hbhAkAemIt+rA5kTmYlFndhpvzkSSM8a2EXsO4XIPgGWCTT ++tQKS/tTly0ADMjN/TVy11+9d6zcqadNVuHXHGtR4W0GR ++-----END RSA PRIVATE KEY----- ++ +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_rsa_key.pub b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_rsa_key.pub +new file mode 100644 +index 0000000000..6d1d90c888 +--- /dev/null ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/ssh_host_rsa_key.pub +@@ -0,0 +1,24 @@ ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++---- BEGIN SSH2 PUBLIC KEY ---- ++AAAAB3NzaC1yc2EAAAADAQABAAAAgQDCZX+4FBDwZIh9y/Uxee1VJnEXlowpz2yDKwj8 ++semM4q843337zbNfxHmladB1lpz2NqyxI175xMIJuDxogyZdsOxGnFAzAnthR4dqL/RW ++RWzjaxSB6IAO9SPYVVlrpZ+1hsjLW79fwXK/yc8VdhRuWTeQiRgYY2ek8+OKbOqz4Q== ++---- END SSH2 PUBLIC KEY ---- +diff --git a/lib/ssh/test/ssh_sftpd_SUITE_data/test.txt b/lib/ssh/test/ssh_sftpd_SUITE_data/test.txt +index 681bff80a0..0fe8ad4bd2 100644 +--- a/lib/ssh/test/ssh_sftpd_SUITE_data/test.txt ++++ b/lib/ssh/test/ssh_sftpd_SUITE_data/test.txt +@@ -1 +1,20 @@ +-Sftp test file. +\ No newline at end of file ++# %CopyrightBegin% ++# ++# SPDX-License-Identifier: Apache-2.0 ++# ++# Copyright Ericsson AB 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. ++# You may obtain a copy of the License at ++# ++# http://www.apache.org/licenses/LICENSE-2.0 ++# ++# Unless required by applicable law or agreed to in writing, software ++# distributed under the License is distributed on an "AS IS" BASIS, ++# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++# See the License for the specific language governing permissions and ++# limitations under the License. ++# ++# %CopyrightEnd% ++Sftp test file. +diff --git a/lib/ssh/test/ssh_test_lib.erl b/lib/ssh/test/ssh_test_lib.erl +index c53139d376..71ab8aea2e 100644 +--- a/lib/ssh/test/ssh_test_lib.erl ++++ b/lib/ssh/test/ssh_test_lib.erl +@@ -124,7 +124,8 @@ setup_known_host/3, + get_addr_str/0, + file_base_name/2, + kex_strict_negotiated/2, +-event_logged/3 ++event_logged/3, ++remove_comment/1 + ]). + %% logger callbacks and related helpers + -export([log/2, +@@ -1218,10 +1219,12 @@ setup_all_user_keys(DataDir, UserDir) -> + setup_user_key(SshAlg, DataDir, UserDir) -> + file:make_dir(UserDir), + %% Copy private user key to user's dir +- {ok,_} = file:copy(filename:join(DataDir, file_base_name(user_src,SshAlg)), +- filename:join(UserDir, file_base_name(user,SshAlg))), ++ {ok, Priv0} = file:read_file(filename:join(DataDir, file_base_name(user_src,SshAlg))), ++ Priv = remove_comment(Priv0), ++ ok = file:write_file(filename:join(UserDir, file_base_name(user,SshAlg)), Priv), + %% Setup authorized_keys in user's dir +- {ok,Pub} = file:read_file(filename:join(DataDir, file_base_name(user_src,SshAlg)++".pub")), ++ {ok,Pub0} = file:read_file(filename:join(DataDir, file_base_name(user_src,SshAlg)++".pub")), ++ Pub = remove_comment(Pub0), + ok = file:write_file(filename:join(UserDir, "authorized_keys"), + io_lib:format("~n~s~n",[Pub]), + [append]), +@@ -1239,19 +1242,26 @@ setup_host_key_create_dir(SshAlg, DataDir, BaseDir) -> + setup_host_key(SshAlg, DataDir, SysDir) -> + mk_dir_path(SysDir), + %% Copy private host key to system's dir +- {ok,_} = file:copy(filename:join(DataDir, file_base_name(system_src,SshAlg)), +- filename:join(SysDir, file_base_name(system,SshAlg))), ++ {ok,Priv0} = file:read_file(filename:join(DataDir, file_base_name(system_src,SshAlg))), ++ Priv = remove_comment(Priv0), ++ ok = file:write_file(filename:join(SysDir, file_base_name(system,SshAlg)), Priv), + ?ct_log_show_file( filename:join(SysDir, file_base_name(system,SshAlg)) ), + ok. + + setup_known_host(SshAlg, DataDir, UserDir) -> +- {ok,Pub} = file:read_file(filename:join(DataDir, file_base_name(system_src,SshAlg)++".pub")), ++ {ok,Pub0} = file:read_file(filename:join(DataDir, file_base_name(system_src,SshAlg)++".pub")), ++ Pub = remove_comment(Pub0), + S = lists:join(" ", lists:reverse(tl(lists:reverse(string:tokens(binary_to_list(Pub), " "))))), + ok = file:write_file(filename:join(UserDir, "known_hosts"), + io_lib:format("~p~n",[S])), + ?ct_log_show_file( filename:join(UserDir, "known_hosts") ), + ok. + ++remove_comment(Bin) -> ++ Lines = string:split(Bin, "\n", all), ++ FilteredLines = [L || L <- Lines, string:prefix(L, "#") == nomatch], ++ list_to_binary(lists:join("\n", FilteredLines)). ++ + + get_addr_str() -> + {ok, Hostname} = inet:gethostname(), +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42789.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42789.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42789.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42789.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,187 @@ +From ae16300cf0a2abbcc9937a25ef0b1a1cb615c70b Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Tue, 26 May 2026 15:30:34 +0200 +Subject: [PATCH] Merge branch 'ingela/public_key/basic_constraint/OTP-20129' + into maint-27 + +* ingela/public_key/basic_constraint/OTP-20129: + public_key: Update to stricter check as clearly defined in RFC 5280 + +(cherry picked from commit f90ae41645001680404a2011f0baf900871a5409) +--- + lib/public_key/src/pubkey_cert.erl | 34 +++++++---- + lib/public_key/test/public_key_SUITE.erl | 75 ++++++++++++++++++++++-- + 2 files changed, 93 insertions(+), 16 deletions(-) + +diff --git a/lib/public_key/src/pubkey_cert.erl b/lib/public_key/src/pubkey_cert.erl +index f61a8ae8bf..6c9262ab19 100644 +--- a/lib/public_key/src/pubkey_cert.erl ++++ b/lib/public_key/src/pubkey_cert.erl +@@ -713,21 +713,30 @@ validate_extensions(Cert, [], ValidationState = + true when SelfSigned -> + {ValidationState, UserState0}; + true -> +- UserState = validate_ext_key_usage(Cert, UserState0, VerifyFun, endentity), +- {ValidationState#path_validation_state{max_path_length = Len - 1}, +- UserState}; +- false -> +- %% basic_constraint must appear in certs used for digital sign +- %% see 4.2.1.10 in rfc 3280 ++ %% If the cA boolean is not asserted (basic_constraint) ++ %% then the keyCertSign bit in the key usage extension MUST NOT be ++ %% asserted. See 4.2.1.9 in RFC 5280 + case is_digitally_sign_cert(Cert) of + true -> +- missing_basic_constraints(Cert, SelfSigned, ++ missing_basic_constraints(Cert, SelfSigned, + ValidationState, VerifyFun, + UserState0, Len); +- false -> %% Example CRL signer only +- {ValidationState, UserState0} +- end ++ false -> ++ UserState = validate_ext_key_usage(Cert, UserState0, VerifyFun, endentity), ++ {ValidationState#path_validation_state{max_path_length = Len - 1}, ++ UserState} ++ end; ++ false -> ++ %% If the basic constraints extension is not present in a ++ %% version 3 certificate, or the extension is present but the cA boolean ++ %% is not asserted, then the certified public key MUST NOT be used to ++ %% verify certificate signature. ++ missing_basic_constraints(Cert, SelfSigned, ++ ValidationState, VerifyFun, ++ UserState0, Len) + end; ++%% The pathLenConstraint field is meaningful only if cA is set to ++%% TRUE. + validate_extensions(Cert, + [#'Extension'{extnID = ?'id-ce-basicConstraints', + extnValue = +@@ -745,8 +754,9 @@ validate_extensions(Cert, + Length}, + basic_constraint, SelfSigned, + UserState, VerifyFun); +-%% The pathLenConstraint field is meaningful only if cA is set to +-%% TRUE. ++ ++%% cA:false — per RFC 5280 Section 6.1.4(k), treated identically to ++%% absent basicConstraints. The base case clause rejects if intermediate. + validate_extensions(Cert, [#'Extension'{extnID = ?'id-ce-basicConstraints', + extnValue = + #'BasicConstraints'{cA = false}} | +diff --git a/lib/public_key/test/public_key_SUITE.erl b/lib/public_key/test/public_key_SUITE.erl +index a85a5d4b01..a267d48dc2 100644 +--- a/lib/public_key/test/public_key_SUITE.erl ++++ b/lib/public_key/test/public_key_SUITE.erl +@@ -103,6 +103,8 @@ + pkix_path_validation/1, + pkix_path_validation_root_expired/0, + pkix_path_validation_root_expired/1, ++ pkix_path_validation_forged_chain/0, ++ pkix_path_validation_forged_chain/1, + pkix_ext_key_usage/0, + pkix_ext_key_usage/1, + pkix_ext_key_usage_any/0, +@@ -173,6 +175,7 @@ all() -> + pkix_decode_cert, + pkix_path_validation, + pkix_path_validation_root_expired, ++ pkix_path_validation_forged_chain, + pkix_ext_key_usage, + pkix_ext_key_usage_any, + pkix_path_validation_bad_date, +@@ -502,14 +505,14 @@ eddsa_pub() -> + eddsa_pub(Config) when is_list(Config) -> + Datadir = proplists:get_value(data_dir, Config), + {ok, EDDSAPubPem} = file:read_file(filename:join(Datadir, "public_eddsa.pem")), +- [{'SubjectPublicKeyInfo', _, not_encrypted} = Key] = PemEntry = ++ [{'SubjectPublicKeyInfo', _, not_encrypted}] = PemEntry = + public_key:pem_decode(EDDSAPubPem), + EDDSAPubKey = public_key:pem_entry_decode(PemEntry), + true = check_entry_type(EDDSAPubKey, 'ECPoint'), + {_, {namedCurve, ?'id-Ed25519'}} = EDDSAPubKey, +- PrivEntry0 = public_key:pem_entry_encode('SubjectPublicKeyInfo', EDDSAPubKey), ++ EncPemEntry = public_key:pem_entry_encode('SubjectPublicKeyInfo', EDDSAPubKey), + ECPemNoEndNewLines = strip_superfluous_newlines(EDDSAPubPem), +- ECPemNoEndNewLines = strip_superfluous_newlines(public_key:pem_encode([PemEntry])). ++ ECPemNoEndNewLines = strip_superfluous_newlines(public_key:pem_encode([EncPemEntry])). + + eddsa_sign_verify_24_compat(_Config) -> + Key = +@@ -1023,7 +1026,71 @@ pkix_path_validation_root_expired(Config) when is_list(Config) -> + true = public_key:pkix_is_self_signed(Root), + Peer = proplists:get_value(cert, Conf), + {error, {bad_cert, cert_expired}} = public_key:pkix_path_validation(Root, [ICA, Peer], []). +- ++ ++pkix_path_validation_forged_chain() -> ++ [{doc, "Test that end-entity can not be used as intermediate CA"}]. ++pkix_path_validation_forged_chain(Config) when is_list(Config) -> ++ #{cert := Root} = SRootSpec = public_key:pkix_test_root_cert("OTP test server ROOT", []), ++ Exts = [#'Extension'{extnID = ?'id-ce-keyUsage', ++ extnValue = [keyCertSign, digitalSignature]}], ++ #{server_config := ServerOpts0} = ++ public_key:pkix_test_data(#{server_chain => ++ #{root => SRootSpec, ++ intermediates => [[]], ++ peer => [{extensions, Exts}]}, ++ client_chain => ++ #{root => [], ++ intermediates => [], ++ peer => []}} ++ ), ++ {ASN1, Key} = proplists:get_value(key, ServerOpts0), ++ ServerKey = public_key:der_decode(ASN1, Key), ++ ServerCert = public_key:pkix_decode_cert(proplists:get_value(cert, ServerOpts0), otp), ++ [ICA] = [I || I <- proplists:get_value(cacerts, ServerOpts0), ++ not public_key:pkix_is_self_signed(I)], ++ {_, Subject} = public_key:pkix_subject_id(ServerCert), ++ #'ECPrivateKey'{parameters = Params, ++ publicKey = PubKey} = public_key:generate_key({namedCurve, ?'secp256r1'}), ++ Algo = #'PublicKeyAlgorithm'{algorithm= ?'id-ecPublicKey', parameters = Params}, ++ SPKI = #'OTPSubjectPublicKeyInfo'{algorithm = Algo, ++ subjectPublicKey = #'ECPoint'{point = PubKey}}, ++ #'OTPCertificate'{tbsCertificate = ServerTBC} = ServerCert, ++ NewTBC = ServerTBC#'OTPTBSCertificate'{issuer = Subject, ++ subject = {rdnSequence, ++ [[{'AttributeTypeAndValue', ++ {2,5,4,3}, ++ {printableString,"forged server Peer cert"}}], ++ [{'AttributeTypeAndValue', ++ {2,5,4,7}, ++ {printableString,"Stockholm"}}], ++ [{'AttributeTypeAndValue',{2,5,4,6},"SE"}], ++ [{'AttributeTypeAndValue', ++ {2,5,4,10}, ++ {printableString,"erlang"}}], ++ [{'AttributeTypeAndValue', ++ {2,5,4,11}, ++ {printableString,"automated testing"}}]]}, ++ subjectPublicKeyInfo = SPKI, ++ extensions = []}, ++ ForgedCert = public_key:pkix_sign(NewTBC, ServerKey), ++ Fun = fun(_, _, {bad_cert, _} = R, _) -> ++ {fail, R}; ++ (_, _, {extension, _}, UserState) -> ++ {unknown, UserState}; ++ (_, _, valid, UserState) -> ++ {valid, UserState}; ++ (OTPCert, _, valid_peer, UserState) -> ++ case public_key:pkix_verify_hostname(OTPCert, ++ [{dns_id, net_adm:localhost()}], []) of ++ true -> ++ {valid, UserState}; ++ false -> ++ {fail, {bad_cert, hostname_check_failed}} ++ end ++ end, ++ {error, Err} = public_key:pkix_path_validation(Root, [ICA, ServerCert, ForgedCert], ++ [{verify_fun, {Fun, []}}]). ++ + pkix_ext_key_usage() -> + [{doc, "If extended key usage is a critical extension in a CA (usually not included) make sure it is compatible with keyUsage extension"}]. + pkix_ext_key_usage(Config) when is_list(Config) -> +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42790.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42790.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42790.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42790.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,476 @@ +From c361f5b45ffd7e963f24aa98bc24797cafe42991 Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Tue, 26 May 2026 15:30:35 +0200 +Subject: [PATCH] Merge branch + 'ingela/maint-27/ssl/public_key/hostname-check-fallback/OTP-20130' into + maint-27 + +* ingela/maint-27/ssl/public_key/hostname-check-fallback/OTP-20130: + ssl: Improve error handling due to public_key updates. + public_key: Add new error + public_key: Adhere to RFC 9525 + +# Conflicts: +# lib/ssl/src/ssl.app.src + +(cherry picked from commit cf547030c1e93290decf093d0338f342eb99d9e4) + +Debian note: the hunk touching lib/ssl/src/ssl.app.src is dropped. In upstream +git that runtime_dependencies entry is the unsubstituted placeholder +"public_key-@OTP-NNNNN@", which upstream's release process resolves before the +tarball is rolled; the released 27.3.4.1 tarball already carries the literal +"public_key-1.16.4". Applying it here would write an unresolved placeholder +into the shipped ssl.app. +--- + lib/public_key/src/pubkey_cert.erl | 24 ++++--- + lib/public_key/src/public_key.erl | 36 +++-------- + lib/public_key/test/pkits_SUITE.erl | 37 +++++++---- + lib/public_key/test/public_key_SUITE.erl | 82 +++++++++--------------- + lib/ssl/src/ssl_handshake.erl | 30 +++++---- + lib/ssl/test/ssl_api_SUITE.erl | 2 +- + lib/ssl/test/ssl_cert_tests.erl | 4 +- + lib/ssl/test/ssl_sni_SUITE.erl | 17 ++--- + 8 files changed, 109 insertions(+), 123 deletions(-) + +diff --git a/lib/public_key/src/pubkey_cert.erl b/lib/public_key/src/pubkey_cert.erl +index 6c9262ab19..488e442af4 100644 +--- a/lib/public_key/src/pubkey_cert.erl ++++ b/lib/public_key/src/pubkey_cert.erl +@@ -237,18 +237,24 @@ validate_names(Cert, Permit, Exclude, Last, UserState, VerifyFun) -> + AltSubject#'Extension'.extnValue + end, + +- case (is_permitted(Name, Permit) andalso +- is_permitted(AltNames, Permit) andalso +- (not is_excluded(Name, Exclude)) andalso +- (not is_excluded(AltNames, Exclude))) of +- true -> +- UserState; +- false -> +- verify_fun(Cert, {bad_cert, name_not_permitted}, +- UserState, VerifyFun) ++ case is_permitted_name(Name, Permit, Exclude) of ++ false -> ++ verify_fun(Cert, {bad_cert, distinguished_name_not_permitted}, ++ UserState, VerifyFun); ++ true -> ++ case is_permitted_name(AltNames, Permit, Exclude) of ++ false -> ++ verify_fun(Cert, {bad_cert, name_not_permitted}, ++ UserState, VerifyFun); ++ true -> ++ UserState ++ end + end + end. + ++is_permitted_name(Name, Permit, Exclude) -> ++ (is_permitted(Name, Permit) andalso (not is_excluded(Name, Exclude))). ++ + %%-------------------------------------------------------------------- + -spec validate_signature(#cert{}, DER::binary(), + term(),term(), term(), fun()) -> term() | no_return(). +diff --git a/lib/public_key/src/public_key.erl b/lib/public_key/src/public_key.erl +index af359c5394..a5e4397433 100644 +--- a/lib/public_key/src/public_key.erl ++++ b/lib/public_key/src/public_key.erl +@@ -306,7 +306,9 @@ The value of the issuer part of a certificate. + -doc """ + The reason that a certifcate gets rejected by the certificate path validation. + """. +--type bad_cert_reason() :: cert_expired | invalid_issuer | invalid_signature | name_not_permitted | ++-type bad_cert_reason() :: cert_expired | invalid_issuer | invalid_signature | ++ distinguished_name_not_permitted | ++ name_not_permitted | + missing_basic_constraint | invalid_key_usage | duplicate_cert_in_path | + {key_usage_mismatch, term()} | + {'policy_requirement_not_met', term()} | {'invalid_policy_mapping', term()} | +@@ -1647,7 +1649,11 @@ Explanations of reasons for a bad certificate: + - **invalid_signature** - Certificate was not signed by its issuer certificate + in the chain. + +-- **name_not_permitted** - Invalid Subject Alternative Name extension. ++- **distinguished_name_not_permitted** - Subject Name does not adhere to name constraints ++ which is mandatory in RFC 5280. ++ ++- **name_not_permitted** - Subject Alternative Name does not adhere to name constraints, ++ which is optional in RFC 5280. + + - **missing_basic_constraint** - Certificate, required to have the basic + constraints extension, does not have a basic constraints extension. +@@ -1901,26 +1907,8 @@ pkix_verify_hostname(Cert = #'OTPCertificate'{tbsCertificate = TbsCert}, Referen + %% PresentedIDs example: [{dNSName,"ewstest.ericsson.com"}, {dNSName,"www.ericsson.com"}]} + case PresentedIDs of + [] -> +- %% Fallback to CN-ids [rfc6125, ch6] +- case TbsCert#'OTPTBSCertificate'.subject of +- {rdnSequence,RDNseq} -> +- PresentedCNs = +- [{cn, to_string(V)} +- || ATVs <- RDNseq, % RDNseq is list-of-lists +- #'AttributeTypeAndValue'{type = ?'id-at-commonName', +- value = {_T,V}} <- ATVs +- % _T = kind of string (teletexString etc) +- ], +- %% Example of PresentedCNs: [{cn,"www.ericsson.se"}] +- %% match ReferenceIDs to PresentedCNs +- verify_hostname_match_loop(verify_hostname_fqnds(ReferenceIDs, FqdnFun), +- PresentedCNs, +- MatchFun, FailCB, Cert); +- +- _ -> +- false +- end; +- _ -> ++ false; ++ _ -> + %% match ReferenceIDs to PresentedIDs + case verify_hostname_match_loop(ReferenceIDs, PresentedIDs, + MatchFun, FailCB, Cert) of +@@ -2805,10 +2793,6 @@ verify_hostname_fqnds(L, FqdnFun) -> + verify_hostname_match_default(Ref, Pres) -> + verify_hostname_match_default0(to_lower_ascii(Ref), to_lower_ascii(Pres)). + +-verify_hostname_match_default0(FQDN=[_|_], {cn,FQDN}) -> +- not lists:member($*, FQDN); +-verify_hostname_match_default0(FQDN=[_|_], {cn,Name=[_|_]}) -> +- verify_hostname_match_wildcard(FQDN, Name); + verify_hostname_match_default0({dns_id,R}, {dNSName,P}) -> + R==P; + verify_hostname_match_default0({uri_id,R}, {uniformResourceIdentifier,P}) -> +diff --git a/lib/public_key/test/pkits_SUITE.erl b/lib/public_key/test/pkits_SUITE.erl +index 29a5e13bda..94b10d9a62 100644 +--- a/lib/public_key/test/pkits_SUITE.erl ++++ b/lib/public_key/test/pkits_SUITE.erl +@@ -1249,19 +1249,30 @@ valid_DN_name_constraints(Config) when is_list(Config) -> + invalid_DN_name_constraints() -> + [{doc,"Name constraints tests"}]. + invalid_DN_name_constraints(Config) when is_list(Config) -> +- run([{ "4.13.2", "Invalid DN nameConstraints Test2 EE", {bad_cert, name_not_permitted}}, +- { "4.13.3", "Invalid DN nameConstraints Test3 EE", {bad_cert, name_not_permitted}}, +- { "4.13.7", "Invalid DN nameConstraints Test7 EE", {bad_cert, name_not_permitted}}, +- { "4.13.8", "Invalid DN nameConstraints Test8 EE", {bad_cert, name_not_permitted}}, +- { "4.13.9", "Invalid DN nameConstraints Test9 EE", {bad_cert, name_not_permitted}}, +- { "4.13.10", "Invalid DN nameConstraints Test10 EE",{bad_cert, name_not_permitted}}, +- { "4.13.12", "Invalid DN nameConstraints Test12 EE",{bad_cert, name_not_permitted}}, +- { "4.13.13", "Invalid DN nameConstraints Test13 EE",{bad_cert, name_not_permitted}}, +- { "4.13.15", "Invalid DN nameConstraints Test15 EE",{bad_cert, name_not_permitted}}, +- { "4.13.16", "Invalid DN nameConstraints Test16 EE",{bad_cert, name_not_permitted}}, +- { "4.13.17", "Invalid DN nameConstraints Test17 EE",{bad_cert, name_not_permitted}}, ++ run([{ "4.13.2", "Invalid DN nameConstraints Test2 EE", ++ {bad_cert, distinguished_name_not_permitted}}, ++ { "4.13.3", "Invalid DN nameConstraints Test3 EE", ++ {bad_cert, name_not_permitted}}, ++ { "4.13.7", "Invalid DN nameConstraints Test7 EE", ++ {bad_cert, distinguished_name_not_permitted}}, ++ { "4.13.8", "Invalid DN nameConstraints Test8 EE", ++ {bad_cert, distinguished_name_not_permitted}}, ++ { "4.13.9", "Invalid DN nameConstraints Test9 EE", ++ {bad_cert, distinguished_name_not_permitted}}, ++ { "4.13.10", "Invalid DN nameConstraints Test10 EE", ++ {bad_cert, distinguished_name_not_permitted}}, ++ { "4.13.12", "Invalid DN nameConstraints Test12 EE", ++ {bad_cert, distinguished_name_not_permitted}}, ++ { "4.13.13", "Invalid DN nameConstraints Test13 EE", ++ {bad_cert, distinguished_name_not_permitted}}, ++ { "4.13.15", "Invalid DN nameConstraints Test15 EE", ++ {bad_cert, distinguished_name_not_permitted}}, ++ { "4.13.16", "Invalid DN nameConstraints Test16 EE", ++ {bad_cert, distinguished_name_not_permitted}}, ++ { "4.13.17", "Invalid DN nameConstraints Test17 EE", ++ {bad_cert, distinguished_name_not_permitted}}, + { "4.13.20", "Invalid DN nameConstraints Test20 EE", +- {bad_cert, name_not_permitted}}]). ++ {bad_cert, distinguished_name_not_permitted}}]). + + valid_rfc822_name_constraints() -> + [{doc,"Name constraints tests"}]. +@@ -1291,7 +1302,7 @@ invalid_DN_and_rfc822_name_constraints(Config) when is_list(Config) -> + run([{ "4.13.28", "Invalid DN and RFC822 nameConstraints Test28 EE", + {bad_cert, name_not_permitted}}, + { "4.13.29", "Invalid DN and RFC822 nameConstraints Test29 EE", +- {bad_cert, name_not_permitted}}]). ++ {bad_cert, distinguished_name_not_permitted}}]). + + valid_dns_name_constraints() -> + [{doc,"Name constraints tests"}]. +diff --git a/lib/public_key/test/public_key_SUITE.erl b/lib/public_key/test/public_key_SUITE.erl +index a267d48dc2..14260c1f69 100644 +--- a/lib/public_key/test/public_key_SUITE.erl ++++ b/lib/public_key/test/public_key_SUITE.erl +@@ -1229,25 +1229,14 @@ pkix_verify_hostname_cn(Config) -> + DataDir = proplists:get_value(data_dir, Config), + {ok,Bin} = file:read_file(filename:join(DataDir,"pkix_verify_hostname_cn.pem")), + Cert = public_key:pkix_decode_cert(element(2,hd(public_key:pem_decode(Bin))), otp), +- +- %% Check that 1) only CNs are checked, +- %% 2) an empty label does not match a wildcard and +- %% 3) a wildcard does not match more than one label ++ %% Fallback hostname check against CommonName is no longer allowed + false = public_key:pkix_verify_hostname(Cert, [{dns_id,"erlang.org"}, + {dns_id,"foo.EXAMPLE.com"}, + {dns_id,"b.a.foo.EXAMPLE.com"}]), +- +- %% Check that a hostname is extracted from a https-uri and used for checking: +- true = public_key:pkix_verify_hostname(Cert, [{uri_id,"HTTPS://EXAMPLE.com"}]), +- +- %% Check wildcard matching one label: +- true = public_key:pkix_verify_hostname(Cert, [{dns_id,"a.foo.EXAMPLE.com"}]), +- +- %% Check wildcard with surrounding chars matches one label: +- true = public_key:pkix_verify_hostname(Cert, [{dns_id,"accb.bar.EXAMPLE.com"}]), +- +- %% Check that a wildcard with surrounding chars matches an empty string: +- true = public_key:pkix_verify_hostname(Cert, [{uri_id,"https://ab.bar.EXAMPLE.com"}]). ++ false = public_key:pkix_verify_hostname(Cert, [{uri_id,"HTTPS://EXAMPLE.com"}]), ++ false = public_key:pkix_verify_hostname(Cert, [{dns_id,"a.foo.EXAMPLE.com"}]), ++ false = public_key:pkix_verify_hostname(Cert, [{dns_id,"accb.bar.EXAMPLE.com"}]), ++ false = public_key:pkix_verify_hostname(Cert, [{uri_id,"https://ab.bar.EXAMPLE.com"}]). + + %%-------------------------------------------------------------------- + %% To generate the PEM file contents: +@@ -1297,63 +1286,50 @@ pkix_verify_hostname_subjAltName(Config) -> + ok. + + %%-------------------------------------------------------------------- +-%% Uses the pem-file for pkix_verify_hostname_cn +-%% Subject: C=SE, CN=example.com, CN=*.foo.example.com, CN=a*b.bar.example.com, O=erlang.org ++%% Uses the pem-file for pkix_verify_hostname_subjAltName.pem ++%% Subject: Subject Alt Names: ++%% [{dNSName,"kb.example.org"}, ++%% {uniformResourceIdentifier,"http://www.example.org"}, ++%% {uniformResourceIdentifier,"https://wws.example.org"}]}] + pkix_verify_hostname_options(Config) -> + DataDir = proplists:get_value(data_dir, Config), +- {ok,Bin} = file:read_file(filename:join(DataDir,"pkix_verify_hostname_cn.pem")), ++ {ok,Bin} = file:read_file(filename:join(DataDir,"pkix_verify_hostname_subjAltName.pem")), + Cert = public_key:pkix_decode_cert(element(2,hd(public_key:pem_decode(Bin))), otp), + + %% Check that the fail_callback is called and is presented the correct certificate: +- true = public_key:pkix_verify_hostname(Cert, [{dns_id,"erlang.org"}], ++ true = public_key:pkix_verify_hostname(Cert, [{dns_id,"kb.example.org"}], + [{fail_callback, + fun(#'OTPCertificate'{}=C) when C==Cert -> + true; % To test the return value matters + (#'OTPCertificate'{}=C) -> + ct:log("~p:~p: Wrong cert:~n~p~nExpect~n~p", + [?MODULE, ?LINE, C, Cert]), +- ct:fail("Wrong cert, see log"); +- (C) -> ++ ct:fail("Wrong cert, see log"); ++ (C) -> + ct:log("~p:~p: Bad cert: ~p",[?MODULE,?LINE,C]), +- ct:fail("Bad cert, see log") ++ ct:fail("Bad cert, see log") + end}]), +- +- %% Check the callback for user-provided match functions: +- true = public_key:pkix_verify_hostname(Cert, [{dns_id,"very.wrong.domain"}], +- [{match_fun, +- fun("very.wrong.domain", {cn,"example.com"}) -> +- true; +- (_, _) -> +- false +- end}]), +- false = public_key:pkix_verify_hostname(Cert, [{dns_id,"not.example.com"}], ++ false = public_key:pkix_verify_hostname(Cert, [{dns_id,"not.example.org"}], + [{match_fun, fun(_, _) -> default end}]), +- true = public_key:pkix_verify_hostname(Cert, [{dns_id,"example.com"}], ++ true = public_key:pkix_verify_hostname(Cert, [{dns_id,"kb.example.org"}], + [{match_fun, fun(_, _) -> default end}]), + + %% Check the callback for user-provided fqdn extraction: + true = public_key:pkix_verify_hostname(Cert, [{uri_id,"some://very.wrong.domain"}], +- [{fqdn_fun, +- fun({uri_id, "some://very.wrong.domain"}) -> +- "example.com"; +- (_) -> +- "" +- end}]), +- true = public_key:pkix_verify_hostname(Cert, [{uri_id,"https://example.com"}], +- [{fqdn_fun, fun(_) -> default end}]), ++ [{fqdn_fun, ++ fun({uri_id, "some://very.wrong.domain"}) -> ++ "kb.example.org"; ++ (_) -> ++ "" ++ end}]), ++ true = public_key:pkix_verify_hostname(Cert, [{uri_id,"https://wws.example.org"}], ++ [{fqdn_fun, fun(_) -> default end}]), + false = public_key:pkix_verify_hostname(Cert, [{uri_id,"some://very.wrong.domain"}]), + +- true = public_key:pkix_verify_hostname(Cert, [{dns_id,"example.com"}]), +- true = public_key:pkix_verify_hostname(Cert, [{dns_id,"abb.bar.example.com"}]), +- false = public_key:pkix_verify_hostname(Cert, [{dns_id,"example.com"}, +- {dns_id,"abb.bar.example.com"}], +- [{fqdn_fun,fun(_)->undefined end}]), +- %% Test that a common name is matched fully, that is do not allow prefix matches +- %% with less dots (".") +- {ok, PrefixBin} = file:read_file(filename:join(DataDir,"prefix-dots.pem")), +- PrefixCert = public_key:pkix_decode_cert(element(2,hd(public_key:pem_decode(PrefixBin))), otp), +- true = public_key:pkix_verify_hostname(PrefixCert, [{dns_id,"..a"}]), +- false = public_key:pkix_verify_hostname(PrefixCert, [{dns_id,".a"}]). ++ true = public_key:pkix_verify_hostname(Cert, ++ [{dns_id,"foobar.example.org"}], ++ [{match_fun, ++ public_key:pkix_verify_hostname_match_fun(https)}]). + + %%-------------------------------------------------------------------- + %% To generate the PEM file contents: +diff --git a/lib/ssl/src/ssl_handshake.erl b/lib/ssl/src/ssl_handshake.erl +index a96ba21034..94c0b7487d 100644 +--- a/lib/ssl/src/ssl_handshake.erl ++++ b/lib/ssl/src/ssl_handshake.erl +@@ -2174,8 +2174,10 @@ path_validation_alert({bad_cert, invalid_signature}, _, _) -> + ?ALERT_REC(?FATAL, ?BAD_CERTIFICATE, invalid_signature); + path_validation_alert({bad_cert, unsupported_signature}, _, _) -> + ?ALERT_REC(?FATAL, ?UNSUPPORTED_CERTIFICATE, unsupported_signature); ++path_validation_alert({bad_cert, distinguished_name_not_permitted}, _, _) -> ++ ?ALERT_REC(?FATAL, ?BAD_CERTIFICATE, distinguised_name_not_permitted); + path_validation_alert({bad_cert, name_not_permitted}, _, _) -> +- ?ALERT_REC(?FATAL, ?BAD_CERTIFICATE, name_not_permitted); ++ ?ALERT_REC(?FATAL, ?BAD_CERTIFICATE, subject_alt_name_not_permitted); + path_validation_alert({bad_cert, unknown_critical_extension}, _, _) -> + ?ALERT_REC(?FATAL, ?UNSUPPORTED_CERTIFICATE, unknown_critical_extension); + path_validation_alert({bad_cert, {revoked, _}}, _, _) -> +@@ -2188,12 +2190,20 @@ path_validation_alert({bad_cert, unknown_ca}, _, _) -> + ?ALERT_REC(?FATAL, ?UNKNOWN_CA); + path_validation_alert({bad_cert, hostname_check_failed}, ServerName, #cert{otp = PeerCert}) -> + SubjAltNames = subject_altnames(PeerCert), +- ?ALERT_REC(?FATAL, ?HANDSHAKE_FAILURE,{bad_cert, {hostname_check_failed, {requested, ServerName}, +- {received, SubjAltNames}}}); ++ case SubjAltNames of ++ [] -> ++ ?ALERT_REC(?FATAL, ?BAD_CERTIFICATE, ++ {bad_cert, {hostname_check_failed, missing_subject_altnames}}); ++ [_ |_ ] -> ++ ?ALERT_REC(?FATAL, ?BAD_CERTIFICATE, ++ {bad_cert, {hostname_check_failed, {requested, ServerName}, ++ {received, SubjAltNames}}}) ++ end; + path_validation_alert({bad_cert, invalid_ext_keyusage}, _, _) -> %% Detected by public key +- ?ALERT_REC(?FATAL, ?UNSUPPORTED_CERTIFICATE, {invalid_ext_keyusage, +- "CA cert purpose anyExtendedKeyUsage" +- "and extended-key-usage extension marked critical is not allowed"}); ++ ?ALERT_REC(?FATAL, ?UNSUPPORTED_CERTIFICATE, ++ {invalid_ext_keyusage, ++ "CA cert purpose anyExtendedKeyUsage" ++ "and extended-key-usage extension marked critical is not allowed"}); + path_validation_alert({bad_cert, {invalid_ext_keyusage, ExtKeyUses}}, _, _) -> + Uses = extkey_oids_to_names(ExtKeyUses, []), + ?ALERT_REC(?FATAL, ?UNSUPPORTED_CERTIFICATE, {invalid_ext_keyusage, Uses}); +@@ -3971,12 +3981,10 @@ supported_cert_signs([default|Signs]) -> + supported_cert_signs(Signs) -> + Signs. + +-subject_altnames(#'OTPCertificate'{tbsCertificate = TBSCert} = OTPCert) -> ++subject_altnames(#'OTPCertificate'{tbsCertificate = TBSCert}) -> + Extensions = extensions_list(TBSCert#'OTPTBSCertificate'.extensions), +- %% Fallback to CN-ids +- {_, Names} = public_key:pkix_subject_id(OTPCert), +- subject_altnames(Extensions, Names). +- ++ subject_altnames(Extensions, []). ++ + subject_altnames([], Names) -> + Names; + subject_altnames([#'Extension'{extnID = ?'id-ce-subjectAltName', +diff --git a/lib/ssl/test/ssl_api_SUITE.erl b/lib/ssl/test/ssl_api_SUITE.erl +index 107b1e0fee..30a3ef946c 100644 +--- a/lib/ssl/test/ssl_api_SUITE.erl ++++ b/lib/ssl/test/ssl_api_SUITE.erl +@@ -1105,7 +1105,7 @@ handshake_continue_change_verify(Config) when is_list(Config) -> + {verify, verify_none} + | ClientOpts], Config)}, + {continue_options, [{verify, verify_peer} | ClientOpts]}]), +- ssl_test_lib:check_client_alert(Client, handshake_failure). ++ ssl_test_lib:check_client_alert(Client, bad_certificate). + + %%------------------------------------------------------------------ + handshake_hello_postpone_opts_verify() -> +diff --git a/lib/ssl/test/ssl_cert_tests.erl b/lib/ssl/test/ssl_cert_tests.erl +index 9e8cf3cf60..65d6845650 100644 +--- a/lib/ssl/test/ssl_cert_tests.erl ++++ b/lib/ssl/test/ssl_cert_tests.erl +@@ -293,10 +293,10 @@ client_auth_sni(Config) when is_list(Config) -> + ServerOpts = [{cacerts, [IntermidiateCA]} | + proplists:delete(cacertfile, ServerOpts0)], + %% Basic test if hostname check is not performed the connection will succeed +- ssl_test_lib:basic_alert(ClientOpts, ServerOpts0, Config, handshake_failure), ++ ssl_test_lib:basic_alert(ClientOpts, ServerOpts0, Config, bad_certificate), + %% Also test that user verify_fun is run. + %% If user verify fun is not used the ALERT will be unknown_ca +- ssl_test_lib:basic_alert(ClientOpts, ServerOpts, Config, handshake_failure). ++ ssl_test_lib:basic_alert(ClientOpts, ServerOpts, Config, bad_certificate). + + %%-------------------------------------------------------------------- + client_auth_seelfsigned_peer() -> +diff --git a/lib/ssl/test/ssl_sni_SUITE.erl b/lib/ssl/test/ssl_sni_SUITE.erl +index 0002e20441..7fce94ca8d 100644 +--- a/lib/ssl/test/ssl_sni_SUITE.erl ++++ b/lib/ssl/test/ssl_sni_SUITE.erl +@@ -190,7 +190,7 @@ sni_no_match(Config) -> + ClientOptions = ssl_test_lib:ssl_options([{server_name_indication, HostName} | + proplists:get_value(client_opts, Config)], Config), + ServerOptions = ssl_test_lib:ssl_options(DefaultConf, Config), +- basic_sni_alert_test(ServerNode, ServerOptions, ClientNode, ClientOptions, HostName, handshake_failure). ++ basic_sni_alert_test(ServerNode, ServerOptions, ClientNode, ClientOptions, HostName, bad_certificate). + + sni_no_match_fun(Config) -> + {ClientNode, ServerNode, HostName} = ssl_test_lib:run_where(Config), +@@ -242,11 +242,11 @@ dns_name(Config) -> + Version = ssl_test_lib:n_version(proplists:get_value(version, Config)), + ServerConf = ssl_test_lib:sig_algs(rsa, Version) ++ ServerOpts0, + ClientConf = ssl_test_lib:sig_algs(rsa, Version) ++ ClientOpts0, +- unsuccessful_connect(ServerConf, [{verify, verify_peer} | ClientConf], undefined, Config, handshake_failure), ++ unsuccessful_connect(ServerConf, [{verify, verify_peer} | ClientConf], undefined, Config, bad_certificate), + successful_connect(ServerConf, [{verify, verify_peer}, + {server_name_indication, Hostname} | ClientConf], undefined, Config), + unsuccessful_connect(ServerConf, [{verify, verify_peer}, {server_name_indication, "foo"} | ClientConf], +- undefined, Config, handshake_failure), ++ undefined, Config, bad_certificate), + successful_connect(ServerConf, [{verify, verify_peer}, {server_name_indication, disable} | ClientConf], + undefined, Config). + +@@ -302,8 +302,8 @@ no_ip_fallback(Config) -> + ServerConf = ssl_test_lib:sig_algs(rsa, Version) ++ ServerOpts0, + ClientConf = ssl_test_lib:sig_algs(rsa, Version) ++ ClientOpts0, + successful_connect(ServerConf, [{verify, verify_peer} | ClientConf], Hostname, Config), +- unsuccessful_connect(ServerConf, [{verify, verify_peer} | ClientConf], IP, Config, handshake_failure), +- unsuccessful_connect(ServerConf, [{verify, verify_peer} | ClientConf], IPStr, Config, handshake_failure). ++ unsuccessful_connect(ServerConf, [{verify, verify_peer} | ClientConf], IP, Config, bad_certificate), ++ unsuccessful_connect(ServerConf, [{verify, verify_peer} | ClientConf], IPStr, Config, bad_certificate). + + dns_name_reuse(Config) -> + SNIHostname = "OTP.test.server", +@@ -329,7 +329,8 @@ dns_name_reuse(Config) -> + + {ClientNode, ServerNode, Hostname} = ssl_test_lib:run_where(Config), + +- unsuccessful_connect(ServerConf, [{verify, verify_peer} | ClientConf], undefined, Config, handshake_failure), ++ unsuccessful_connect(ServerConf, [{verify, verify_peer} | ClientConf], undefined, Config, ++ bad_certificate), + + Server = + ssl_test_lib:start_server([{node, ServerNode}, {port, 0}, +@@ -359,7 +360,7 @@ dns_name_reuse(Config) -> + {mfa, {ssl_test_lib, session_info_result, []}}, + {from, self()}, {options, [{verify, verify_peer} | ClientConf]}]), + +- ssl_test_lib:check_client_alert(Client1, handshake_failure), ++ ssl_test_lib:check_client_alert(Client1, bad_certificate), + ssl_test_lib:close(Client0). + + +@@ -412,7 +413,7 @@ customize_hostname_check(Config) when is_list(Config) -> + {options, [{verify, verify_peer}, + {server_name_indication, "other.example.org"} | ClientOpts]} + ]), +- ssl_test_lib:check_client_alert(Server, Client1, handshake_failure). ++ ssl_test_lib:check_client_alert(Server, Client1, bad_certificate). + + sni_no_trailing_dot() -> + [{doc,"Test that sni may not include a triling dot"}]. +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42791.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42791.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42791.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42791.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,125 @@ +From 72ba4f7e392b4f4d80790fef6b91b73dd72ef21f Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Tue, 26 May 2026 15:30:38 +0200 +Subject: [PATCH] Merge branch + 'kuba/maint-27/public_key/ocsp_responder_validity_period_fix/OTP-20112' into + maint-27 + +* kuba/maint-27/public_key/ocsp_responder_validity_period_fix/OTP-20112: + public_key: Validate OCSP responder certificate validity period + +(cherry picked from commit 5c0314c80d9e17b57fb378560ecb75fb38b321ac) +--- + lib/public_key/src/pubkey_cert.erl | 3 +- + lib/public_key/src/pubkey_ocsp.erl | 11 ++++++ + lib/public_key/test/pubkey_ocsp_SUITE.erl | 47 ++++++++++++++++++++++- + 3 files changed, 59 insertions(+), 2 deletions(-) + +diff --git a/lib/public_key/src/pubkey_cert.erl b/lib/public_key/src/pubkey_cert.erl +index 488e442af4..64334383c1 100644 +--- a/lib/public_key/src/pubkey_cert.erl ++++ b/lib/public_key/src/pubkey_cert.erl +@@ -47,7 +47,8 @@ + match_name/3, + extensions_list/1, + cert_auth_key_id/1, +- time_str_2_gregorian_sec/1 ++ time_str_2_gregorian_sec/1, ++ parse_and_check_validity_dates/1 + ]). + + %% Generate test data +diff --git a/lib/public_key/src/pubkey_ocsp.erl b/lib/public_key/src/pubkey_ocsp.erl +index 89355e8937..fc08623fd9 100644 +--- a/lib/public_key/src/pubkey_ocsp.erl ++++ b/lib/public_key/src/pubkey_ocsp.erl +@@ -216,6 +216,17 @@ is_responder_cert({byKey, Key}, #cert{otp = Cert}) -> + + is_authorized_responder(CombinedResponderCert = #cert{otp = ResponderCert}, + IssuerCert, IsTrustedResponderFun) -> ++ case pubkey_cert:parse_and_check_validity_dates(ResponderCert) of ++ ok -> ++ check_responder_authorization(CombinedResponderCert, ++ ResponderCert, IssuerCert, ++ IsTrustedResponderFun); ++ _ExpiredOrError -> ++ not_authorized_responder ++ end. ++ ++check_responder_authorization(CombinedResponderCert, ResponderCert, ++ IssuerCert, IsTrustedResponderFun) -> + Case1 = + %% the CA who issued the certificate in question signed the + %% response +diff --git a/lib/public_key/test/pubkey_ocsp_SUITE.erl b/lib/public_key/test/pubkey_ocsp_SUITE.erl +index 480d201c10..f692b4056b 100644 +--- a/lib/public_key/test/pubkey_ocsp_SUITE.erl ++++ b/lib/public_key/test/pubkey_ocsp_SUITE.erl +@@ -105,7 +105,8 @@ + %% Common Test interface functions ----------------------------------- + %%-------------------------------------------------------------------- + all() -> +- [ocsp_test, designated_responder]. ++ [ocsp_test, designated_responder, ++ ocsp_responder_cert_expired, ocsp_responder_cert_not_yet_valid]. + + groups() -> + []. +@@ -260,6 +261,44 @@ designated_responder(Config) when is_list(Config) -> + NonceExt, CACert, IsNotTrustedFun), + ok. + ++%%-------------------------------------------------------------------- ++ocsp_responder_cert_expired() -> ++ [{doc, "Verify that an expired responder certificate is rejected. " ++ "RFC 5280 Section 4.1.2.5 requires validity period checking."}]. ++ocsp_responder_cert_expired(Config) when is_list(Config) -> ++ {ok, OcspResponse} = ++ pubkey_ocsp:decode_response(?OCSP_RESPONSE_DER), ++ ExpiredCert = set_cert_validity(?ISSUER_CERT, ++ {generalTime, "20180101000000Z"}, ++ {generalTime, "20200101000000Z"}), ++ IsTrustedFun = fun(_) -> true end, ++ {error, ocsp_responder_cert_not_found} = ++ pubkey_ocsp:verify_response(OcspResponse, ++ [#cert{otp = ExpiredCert}], ++ ?NONCE, ++ ?ISSUER_CERT, ++ IsTrustedFun), ++ ok. ++ ++%%-------------------------------------------------------------------- ++ocsp_responder_cert_not_yet_valid() -> ++ [{doc, "Verify that a not-yet-valid responder certificate is rejected. " ++ "RFC 5280 Section 4.1.2.5 requires validity period checking."}]. ++ocsp_responder_cert_not_yet_valid(Config) when is_list(Config) -> ++ {ok, OcspResponse} = ++ pubkey_ocsp:decode_response(?OCSP_RESPONSE_DER), ++ FutureCert = set_cert_validity(?ISSUER_CERT, ++ {generalTime, "21000101000000Z"}, ++ {generalTime, "21100101000000Z"}), ++ IsTrustedFun = fun(_) -> true end, ++ {error, ocsp_responder_cert_not_found} = ++ pubkey_ocsp:verify_response(OcspResponse, ++ [#cert{otp = FutureCert}], ++ ?NONCE, ++ ?ISSUER_CERT, ++ IsTrustedFun), ++ ok. ++ + %%-------------------------------------------------------------------- + %% Helpers ----------------------------------------------------------- + %%-------------------------------------------------------------------- +@@ -356,3 +395,9 @@ build_ocsp_response(IssuerName, IssuerKey, NonceExt, SignKey) -> + parameters = asn1_NOVALUE}, + signature = Signature, + certs = asn1_NOVALUE}. ++ ++set_cert_validity(OtpCert, NotBefore, NotAfter) -> ++ TBS = OtpCert#'OTPCertificate'.tbsCertificate, ++ NewValidity = #'Validity'{notBefore = NotBefore, notAfter = NotAfter}, ++ NewTBS = TBS#'OTPTBSCertificate'{validity = NewValidity}, ++ OtpCert#'OTPCertificate'{tbsCertificate = NewTBS}. +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42792-1.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42792-1.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42792-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42792-1.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,32 @@ +From b1fe971c4c1217894705b046be4e94b8c07b7ca7 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?John=20H=C3=B6gberg?= +Date: Wed, 13 May 2026 08:21:36 +0200 +Subject: [PATCH] epmd: Don't clobber errno on dbg_perror + +(cherry picked from commit d5598dbaea92c14d4e421898a6886ef31b63a175) +--- + erts/epmd/src/epmd.c | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/erts/epmd/src/epmd.c b/erts/epmd/src/epmd.c +index e71a0528dd..d64ba895d7 100644 +--- a/erts/epmd/src/epmd.c ++++ b/erts/epmd/src/epmd.c +@@ -476,9 +476,13 @@ static void dbg_gen_printf(int onsyslog,int perr,int from_level, + void dbg_perror(EpmdVars *g,const char *format,...) + { + va_list args; ++ int perr = errno; ++ + va_start(args, format); +- dbg_gen_printf(1,errno,0,g,format,args); ++ dbg_gen_printf(1,perr,0,g,format,args); + va_end(args); ++ ++ errno = perr; + } + + +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42792-2.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42792-2.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42792-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-42792-2.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,59 @@ +From de17b79c97f7e682d56361e8309c0df3798d6cf5 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?John=20H=C3=B6gberg?= +Date: Fri, 8 May 2026 13:33:31 +0200 +Subject: [PATCH] epmd: Improve slow-connection handling + +(cherry picked from commit 865d203e4a6a8f44179eced9e1428f9259e4a3bb) +--- + erts/epmd/src/epmd_srv.c | 23 +++++++++++++---------- + 1 file changed, 13 insertions(+), 10 deletions(-) + +diff --git a/erts/epmd/src/epmd_srv.c b/erts/epmd/src/epmd_srv.c +index 77b0a25fc8..b1f2f8e59f 100644 +--- a/erts/epmd/src/epmd_srv.c ++++ b/erts/epmd/src/epmd_srv.c +@@ -687,18 +687,19 @@ static void do_read(EpmdVars *g,Connection *s) + return; + } + } +- +- s->mod_time = current_time(g); /* Note activity */ +- +- if (s->want == s->got) ++ ++ if (s->want == s->got) + { +- /* Do action and close up */ +- /* Skip header bytes */ ++ /* Note activity. */ ++ s->mod_time = current_time(g); + ++ /* Do action and close up. +/- 2 is to skip the length prefix. */ + do_request(g, s->fd, s, s->buf + 2, s->got - 2); + +- if (!s->keep) +- epmd_conn_close(g,s); /* Normal close */ ++ if (!s->keep) { ++ /* Normal close */ ++ epmd_conn_close(g,s); ++ } + } + } + +@@ -719,9 +720,11 @@ static int do_accept(EpmdVars *g,int listensock) + case EAGAIN: + case ECONNABORTED: + case EINTR: +- return EPMD_FALSE; ++ case EMFILE: ++ case ENFILE: ++ return EPMD_FALSE; + default: +- epmd_cleanup_exit(g,1); ++ epmd_cleanup_exit(g,1); + } + } + +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-47078.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-47078.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-47078.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-47078.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,160 @@ +From 6b0ffc87999276a88321975bc69ed050019a38bd Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Fri, 24 Jul 2026 17:04:11 +0200 +Subject: [PATCH] Merge branch 'john/stdlib/GHSA-rf72-wp7h-jg3x/OTP-20143' into + maint-27 + +* john/stdlib/GHSA-rf72-wp7h-jg3x/OTP-20143: + zip: Fix relative path traversal checking + +(cherry picked from commit 3c629528a5e279532fd56a79d49094dddd064f52) + +Debian note: the deletion of the binary test fixture +lib/stdlib/test/zip_SUITE_data/exploit.zip is dropped, because a quilt patch +cannot represent a binary file change. The rewritten test builds the archive +at runtime with zip:create/3 in priv_dir, so the stale fixture is simply left +in place, unreferenced. +--- + lib/stdlib/src/zip.erl | 49 +++++++++++++++++++++++++++-------- + lib/stdlib/test/zip_SUITE.erl | 30 ++++++++++++++++----- + 2 files changed, 61 insertions(+), 18 deletions(-) + +diff --git a/lib/stdlib/src/zip.erl b/lib/stdlib/src/zip.erl +index e94bfc38fd..ee68920d03 100644 +--- a/lib/stdlib/src/zip.erl ++++ b/lib/stdlib/src/zip.erl +@@ -2340,27 +2340,54 @@ check_valid_location(CWD, FileName) -> + end, + %% check for directory traversal exploit + {IsValid, Name} = +- case check_dir_level(filename:split(FileName), 0) of +- {FileOrDir,Level} when Level < 0 -> ++ case check_dir_level(FileName) of ++ {FileOrDir, invalid} -> + CWD1 = if CWD == "" -> "./"; + true -> CWD + end, + error_logger:format("Illegal path: ~ts, extracting in ~ts~n", + [add_cwd(CWD,FileName),CWD1]), + {false, FileOrDir}; +- _ -> ++ {_FileOrDir, valid} -> + {true, FileName} + end, + {IsValid, string:trim(Name, trailing, "/") ++ TrailingSlash}. + +-check_dir_level([FileOrDir], Level) -> +- {FileOrDir,Level}; +-check_dir_level(["." | Parts], Level) -> +- check_dir_level(Parts, Level); +-check_dir_level([".." | Parts], Level) -> +- check_dir_level(Parts, Level-1); +-check_dir_level([_Dir | Parts], Level) -> +- check_dir_level(Parts, Level+1). ++check_dir_level(FileName) -> ++ %% Normalize the path by rejecting `.`. If we have an invalid path like ++ %% "../hello/.", we will then present it to the filter as "hello". ++ %% ++ %% Note that "." is returned as-is for quirks-compatibility. ++ Parts = [Component || Component <- filename:split(FileName), ++ not string:equal(Component, ".")], ++ case Parts of ++ [_|_] -> ++ try cdl_1(Parts, 0) of ++ Res -> Res ++ catch ++ error:_ -> error({invalid_filename, FileName}) ++ end; ++ [] -> ++ {".", invalid} ++ end. ++ ++cdl_1([".." | Parts], Level) -> ++ case Level > 0 of ++ true -> ++ cdl_1(Parts, Level - 1); ++ false -> ++ %% We must never return ".." when marked invalid since there's no ++ %% way that the filter can do anything sensible with it. It's ++ %% better to crash. ++ Last = lists:last(Parts), ++ false = string:equal(Last, ".."), ++ {Last, invalid} ++ end; ++cdl_1([FileOrDir], Level) -> ++ true = Level >= 0, %Assertion. ++ {FileOrDir, valid}; ++cdl_1([_Dir | Parts], Level) -> ++ cdl_1(Parts, Level + 1). + + get_filename_extra(FileNameLen, ExtraLen, B, GPFlag) -> + try +diff --git a/lib/stdlib/test/zip_SUITE.erl b/lib/stdlib/test/zip_SUITE.erl +index a598aa66b3..8e91df9dc2 100644 +--- a/lib/stdlib/test/zip_SUITE.erl ++++ b/lib/stdlib/test/zip_SUITE.erl +@@ -515,14 +515,28 @@ unzip_options(Config) when is_list(Config) -> + + %% Test that unzip handles directory traversal exploit (OTP-13633) + unzip_traversal_exploit(Config) -> +- DataDir = get_value(data_dir, Config), + PrivDir = get_value(priv_dir, Config), +- ZipName = filename:join(DataDir, "exploit.zip"), +- +- %% $ zipinfo -1 test/zip_SUITE_data/exploit.zip ++ ZipName = filename:join(PrivDir, "exploit.zip"), ++ ++ {ok, {_Name, Bin}} = ++ zip:create("exploit.zip", ++ [{"clash.txt", ++ <<"This is the original file.\n">>}, ++ {"../clash.txt", ++ <<"This file will overwrite the original file.\n">>}, ++ {"../above.txt", ++ <<"This is above the root directory.\n">>}, ++ {"../above/variant.txt", ++ <<"This is also above the root directory.\n">>}, ++ {"subdir/../in_root_dir.txt", ++ <<"This is in the root directory.\n">>}], [memory]), ++ ok = file:write_file(ZipName, Bin), ++ ++ %% $ zipinfo -1 exploit.zip + %% clash.txt + %% ../clash.txt + %% ../above.txt ++ %% ../above/variant.txt + %% subdir/../in_root_dir.txt + + %% create a temp directory +@@ -531,15 +545,17 @@ unzip_traversal_exploit(Config) -> + + ClashFile = filename:join(SubDir,"clash.txt"), + AboveFile = filename:join(SubDir,"above.txt"), ++ VariantFile = filename:join(SubDir,"variant.txt"), + RelativePathFile = filename:join(SubDir,"subdir/../in_root_dir.txt"), + + %% unzip in SubDir +- {ok, [ClashFile, ClashFile, AboveFile, RelativePathFile]} = ++ {ok, [ClashFile, ClashFile, AboveFile, VariantFile, RelativePathFile]} = + zip:unzip(ZipName, [{cwd,SubDir}]), + +- {ok,<<"This file will overwrite other file.\n">>} = ++ {ok,<<"This file will overwrite the original file.\n">>} = + file:read_file(ClashFile), + {ok,_} = file:read_file(AboveFile), ++ {ok,_} = file:read_file(VariantFile), + {ok,_} = file:read_file(RelativePathFile), + + %% clean up +@@ -549,7 +565,7 @@ unzip_traversal_exploit(Config) -> + ok = file:make_dir(SubDir), + + %% unzip in SubDir +- {ok, [ClashFile, AboveFile, RelativePathFile]} = ++ {ok, [ClashFile, AboveFile, VariantFile, RelativePathFile]} = + zip:unzip(ZipName, [{cwd,SubDir},keep_old_files]), + + {ok,<<"This is the original file.\n">>} = +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48855.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48855.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48855.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48855.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,254 @@ +From 754917f1a05e7023423e945b05476345df14c8dd Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Tue, 9 Jun 2026 16:30:34 +0200 +Subject: [PATCH] Merge branch + 'michal/ssh/fix-sftp-readlink-abs-path-leak/OTP-20162' into maint-27 + +* michal/ssh/fix-sftp-readlink-abs-path-leak/OTP-20162: + Fix absolute path leak from SSH_FXP_READLINK + Strip comments when installing ssh keys + Add information about symlinks to hardening guide + Add ssh keys and license headers for ssh_sftpd_SUITE + +# Conflicts: +# lib/ssh/test/ssh_test_lib.erl + +(cherry picked from commit d25347ce870223f0b27170c8fd90584817c6dedf) +--- + lib/ssh/doc/guides/hardening.md | 27 +++++++ + lib/ssh/src/ssh_sftpd.erl | 6 +- + lib/ssh/test/ssh_sftpd_SUITE.erl | 124 ++++++++++++++++++++++++++----- + lib/ssh/test/ssh_test_lib.erl | 1 - + 4 files changed, 138 insertions(+), 20 deletions(-) + +diff --git a/lib/ssh/doc/guides/hardening.md b/lib/ssh/doc/guides/hardening.md +index c092f5c8ef..bfeddac4b1 100644 +--- a/lib/ssh/doc/guides/hardening.md ++++ b/lib/ssh/doc/guides/hardening.md +@@ -252,6 +252,33 @@ It is possible to replace the string with one randomly generated for each + connection attempt. See the reference manual for + [id_string](`t:ssh:id_string_common_option/0`). + ++## SFTP symlink risks ++ ++Symbolic links inside the SFTP root that point outside it can be followed by ++file operations such as `open`, `read`, and `stat`. The `root` option ++confines path resolution done by the daemon itself, but it does not prevent ++the operating system from following symlinks when the daemon opens the ++resulting path. ++ ++Symlinks created by SFTP clients are not exploitable this way: the daemon ++converts the target to an absolute path and clamps it to the configured root ++before writing the link. However, symlinks that already exist inside the root ++directory, or are created through other access channels (shell access, another ++service, etc.), can point outside the root and will be followed. ++ ++Mitigations: ++ ++- **Filesystem permissions** - Run the SSH daemon as a dedicated user with ++ minimal privileges. Ensure that user cannot read or write sensitive files ++ outside the root. ++ ++- **Control the root contents** - Ensure no pre-existing symlinks in the root ++ point outside it, and that no other process can create such links. ++ ++- **OS-level protections** - On Linux, mount the root directory with the ++ `nosymfollow` mount option, or use filesystem namespaces to prevent ++ symlink traversal outside the root. ++ + ## Client connection options + + A client could limit the time for the initial tcp connection establishment with +diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl +index 2f1982277d..0cdd0da62a 100644 +--- a/lib/ssh/src/ssh_sftpd.erl ++++ b/lib/ssh/src/ssh_sftpd.erl +@@ -439,8 +439,10 @@ handle_op(?SSH_FXP_READLINK, ReqId, <>, + {Res, FS1} = FileMod:read_link(AbsPath, FS0), + case Res of + {ok, NewPath} -> +- ssh_xfer:xf_send_name(State#state.xf, ReqId, NewPath, +- #ssh_xfer_attr{type=regular}); ++ AbsTarget = filename:absname(NewPath, filename:dirname(AbsPath)), ++ ChrootedPath = chroot_filename(canonicalize_filename(AbsTarget), State), ++ ssh_xfer:xf_send_name(State#state.xf, ReqId, ChrootedPath, ++ #ssh_xfer_attr{type=regular}); + {error, Error} -> + ssh_xfer:xf_send_status(State#state.xf, ReqId, + ssh_xfer:encode_erlang_status(Error)) +diff --git a/lib/ssh/test/ssh_sftpd_SUITE.erl b/lib/ssh/test/ssh_sftpd_SUITE.erl +index 2c6655ef12..62505bbde2 100644 +--- a/lib/ssh/test/ssh_sftpd_SUITE.erl ++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl +@@ -35,6 +35,7 @@ + + -export([access_outside_root/1, + links/1, ++ links_root/1, + mk_rm_dir/1, + open_close_dir/1, + open_close_file/1, +@@ -96,6 +97,7 @@ all() -> + retrieve_attributes, + set_attributes, + links, ++ links_root, + ver3_rename, + ver3_open_flags, + relpath, +@@ -189,6 +191,14 @@ init_per_testcase(TestCase, Config) -> + SubSystems = [ssh_sftpd:subsystem_spec([{cwd, PrivDir}, + {sftpd_vsn, 6}])], + ssh:daemon(0, [{subsystems, SubSystems}|Options]); ++ links -> ++ SubSystems = [ssh_sftpd:subsystem_spec([{cwd, PrivDir}])], ++ ssh:daemon(0, [{subsystems, SubSystems}|Options]); ++ links_root -> ++ RootDir = filename:join(PrivDir, links_root), ++ ok = file:make_dir(RootDir), ++ SubSystems = [ssh_sftpd:subsystem_spec([{root, RootDir}, {cwd, RootDir}])], ++ ssh:daemon(0, [{subsystems, SubSystems}|Options]); + _ -> + SubSystems = [ssh_sftpd:subsystem_spec( + [{max_handles, ?MAX_HANDLES}, +@@ -529,28 +539,108 @@ real_path(Config) when is_list(Config) -> + %%-------------------------------------------------------------------- + links(Config) when is_list(Config) -> + case os:type() of +- {win32, _} -> +- {skip, "Links are not fully supported by windows"}; +- _ -> +- ReqId = 0, +- {Cm, Channel} = proplists:get_value(sftp, Config), +- PrivDir = proplists:get_value(priv_dir, Config), +- FileName = filename:join(PrivDir, "test.txt"), +- LinkFileName = filename:join(PrivDir, "link_test.txt"), ++ {win32, _} -> ++ {skip, "Links are not fully supported by windows"}; ++ _ -> ++ Sftp = proplists:get_value(sftp, Config), ++ PrivDir = string:trim(proplists:get_value(priv_dir, Config), trailing, "/"), ++ LinkPath = filename:join(".", "link"), + +- {ok, <>, _} = +- create_link(LinkFileName, FileName, Cm, Channel, ReqId), ++ AbsBelowCwd = filename:join(PrivDir, "file"), ++ links_helper(Sftp, PrivDir, 0, LinkPath, AbsBelowCwd, AbsBelowCwd, AbsBelowCwd, AbsBelowCwd), + +- NewReqId = 1, +- {ok, <>, _} +- = read_link(LinkFileName, Cm, Channel, NewReqId), ++ AbsAtCwd = PrivDir, ++ links_helper(Sftp, PrivDir, 1, LinkPath, AbsAtCwd, AbsAtCwd, AbsAtCwd, AbsAtCwd), + ++ AbsAboveCwd = filename:join([PrivDir, "..", "file"]), ++ AbsAboveCwdExpected = filename:join(filename:dirname(PrivDir), "file"), ++ links_helper(Sftp, PrivDir, 2, LinkPath, AbsAboveCwd, AbsAboveCwdExpected, AbsAboveCwd, AbsAboveCwdExpected), + +- true = binary_to_list(Path) == FileName, ++ RelBelowCwd = filename:join(".", "file"), ++ RelBelowCwdExpected = AbsBelowCwd, ++ links_helper(Sftp, PrivDir, 3, LinkPath, RelBelowCwd, RelBelowCwdExpected, RelBelowCwd, RelBelowCwdExpected), + +- ct:log("Path: ~p~n", [binary_to_list(Path)]) ++ RelAtCwd = ".", ++ RelAtCwdExpected = AbsAtCwd, ++ links_helper(Sftp, PrivDir, 4, LinkPath, RelAtCwd, RelAtCwdExpected, RelAtCwd, RelAtCwdExpected), ++ ++ RelAboveCwd = filename:join("..", "file"), ++ RelAboveCwdExpected = AbsAboveCwdExpected, ++ links_helper(Sftp, PrivDir, 5, LinkPath, RelAboveCwd, RelAboveCwdExpected, RelAboveCwd, RelAboveCwdExpected) ++ end. ++ ++%%-------------------------------------------------------------------- ++links_root(Config) when is_list(Config) -> ++ case os:type() of ++ {win32, _} -> ++ {skip, "Links are not fully supported by windows"}; ++ _ -> ++ Sftp = proplists:get_value(sftp, Config), ++ PrivDir = proplists:get_value(priv_dir, Config), ++ Root = filename:join(PrivDir, links_root), ++ LinkPath = filename:join("/", "link"), ++ ++ AbsBelowRoot = filename:join(Root, "file"), ++ AbsBelowRootClient = filename:join("/", "file"), ++ links_helper(Sftp, Root, 0, LinkPath, AbsBelowRoot, AbsBelowRoot, AbsBelowRootClient, AbsBelowRootClient), ++ ++ AbsAtRoot = Root, ++ AbsAtRootExpected = AbsAtRoot, ++ links_helper(Sftp, Root, 1, LinkPath, AbsAtRoot, AbsAtRootExpected, "/", "/"), ++ ++ AbsAboveRoot = filename:join(PrivDir, "file"), ++ AbsAboveRootExpected = AbsAtRootExpected, ++ AbsAboveRootClient = filename:join("/", ".."), ++ links_helper(Sftp, Root, 2, LinkPath, AbsAboveRoot, AbsAboveRootExpected, AbsAboveRootClient, "/"), ++ ++ RelBelowRoot = filename:join(".", "file"), ++ RelBelowRootExpected = filename:join(Root, "file"), ++ RelBelowRootClient = RelBelowRoot, ++ RelBelowRootClientExpected = AbsBelowRootClient, ++ links_helper(Sftp, Root, 3, LinkPath, RelBelowRoot, RelBelowRootExpected, RelBelowRootClient, RelBelowRootClientExpected), ++ ++ RelAtRoot = ".", ++ RelAtRootExpected = Root, ++ links_helper(Sftp, Root, 4, LinkPath, RelAtRoot, RelAtRootExpected, ".", "/"), ++ ++ RelAboveRoot = "../file", ++ RelAboveRootExpected = Root, ++ RelAboveRootClient = filename:join("..", "file"), ++ links_helper(Sftp, Root, 5, LinkPath, RelAboveRoot, RelAboveRootExpected, RelAboveRootClient, "/") ++ end. ++ ++links_helper({Cm, Channel}, Root, ReqId0, LinkPath, RawTarget, RawExpected, ClientTarget, ClientExpected) -> ++ ?CT_LOG("RawTarget: ~p, RawExpected: ~p~nClientTarget: ~p, ClientExpected: ~p~n", ++ [RawTarget, RawExpected, ClientTarget, ClientExpected]), ++ ++ ReqId1 = ReqId0 * 3, ++ LinkLocation = filename:join(Root, "link"), ++ ok = file:make_symlink(RawTarget, LinkLocation), ++ try ++ {ok, <>, _} = ++ read_link(LinkPath, Cm, Channel, ReqId1), ++ ClientActual1 = binary_to_list(ClientActualB1), ++ ClientExpected = ClientActual1, ++ ++ ok = file:delete(LinkLocation), ++ ++ ReqId2 = ReqId1 + 1, ++ {ok, <>, _} = ++ create_link(LinkPath, ClientTarget, Cm, Channel, ReqId2), ++ ++ {ok, RawActual} = file:read_link(LinkLocation), ++ RawExpected = string:trim(RawActual, trailing, "/"), ++ ++ ReqId3 = ReqId2 + 1, ++ {ok, <>, _} = ++ read_link(LinkPath, Cm, Channel, ReqId3), ++ ClientActual2 = binary_to_list(ClientActualB2), ++ ClientExpected = ClientActual2 ++ after ++ file:delete(LinkLocation) + end. + + %%-------------------------------------------------------------------- +diff --git a/lib/ssh/test/ssh_test_lib.erl b/lib/ssh/test/ssh_test_lib.erl +index 71ab8aea2e..0a22704c13 100644 +--- a/lib/ssh/test/ssh_test_lib.erl ++++ b/lib/ssh/test/ssh_test_lib.erl +@@ -1262,7 +1262,6 @@ remove_comment(Bin) -> + FilteredLines = [L || L <- Lines, string:prefix(L, "#") == nomatch], + list_to_binary(lists:join("\n", FilteredLines)). + +- + get_addr_str() -> + {ok, Hostname} = inet:gethostname(), + {ok, {A, B, C, D}} = inet:getaddr(Hostname, inet), +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48856.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48856.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48856.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48856.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,129 @@ +From a9fcaf7bc68d4e4a2c35e19a6f4b09efca6efce3 Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Tue, 9 Jun 2026 16:30:35 +0200 +Subject: [PATCH] Merge branch + 'whaileee/httpc/auth-headers-forward-on-cross-origin/OTP-20155' into maint-27 + +* whaileee/httpc/auth-headers-forward-on-cross-origin/OTP-20155: + inets/httpc: strip sensitive headers on cross-origin redirect + +(cherry picked from commit a446753217488efa1a44854a339c1a6472ab0646) +--- + lib/inets/src/http_client/httpc_response.erl | 22 ++++++++- + lib/inets/test/httpc_SUITE.erl | 52 ++++++++++++++++++++ + 2 files changed, 73 insertions(+), 1 deletion(-) + +diff --git a/lib/inets/src/http_client/httpc_response.erl b/lib/inets/src/http_client/httpc_response.erl +index 9ebea9bdbc..b43b56ddd0 100644 +--- a/lib/inets/src/http_client/httpc_response.erl ++++ b/lib/inets/src/http_client/httpc_response.erl +@@ -411,8 +411,28 @@ redirect(Response = {_, Headers, _}, Request) -> + NewURI = uri_string:normalize( + uri_string:recompose(URIMap)), + HostPort = http_request:normalize_host(TScheme, THost, TPort), +- NewHeaders = ++ NewHeaders0 = + (Request#request.headers)#http_request_h{host = HostPort}, ++ %% RFC 9110 §15.4: strip Authorization, Proxy-Authorization, ++ %% Cookie, Origin, and Referer on cross-origin redirects ++ %% (different host or port). ++ NewHeaders = ++ case Request#request.address of ++ {THost, TPort} -> ++ NewHeaders0; ++ _ -> ++ CrossOriginOther = ["cookie", "origin"], ++ OtherStripped = lists:filter( ++ fun({K, _}) -> ++ not lists:member(string:lowercase(K), CrossOriginOther) ++ end, ++ NewHeaders0#http_request_h.other), ++ NewHeaders0#http_request_h{ ++ authorization = undefined, ++ 'proxy-authorization' = undefined, ++ referer = undefined, ++ other = OtherStripped} ++ end, + NewRequest = + Request#request{redircount = + Request#request.redircount+1, +diff --git a/lib/inets/test/httpc_SUITE.erl b/lib/inets/test/httpc_SUITE.erl +index 801b8b7084..cab0ed37f6 100644 +--- a/lib/inets/test/httpc_SUITE.erl ++++ b/lib/inets/test/httpc_SUITE.erl +@@ -193,6 +193,7 @@ only_simulated() -> + redirect_temporary_redirect, + redirect_permanent_redirect, + redirect_relative_uri, ++ redirect_strips_sensitive_headers, + port_in_host_header, + redirect_port_in_host_header, + relaxed, +@@ -887,6 +888,40 @@ redirect_relative_different_port(Config) when is_list(Config) -> + = httpc:request(post, {URL301, Headers, "text/plain", "foobar"}, + [], RequestOpts, Profile). + %%------------------------------------------------------------------------- ++redirect_strips_sensitive_headers() -> ++ [{doc, "RFC 9110 §15.4: Authorization, Proxy-Authorization, Cookie, " ++ "Referer, and Origin headers MUST NOT be forwarded on cross-origin " ++ "(different host/port) redirects (CVE / GHSA-m75x-4vwg-ggjh)."}]. ++redirect_strips_sensitive_headers(Config) when is_list(Config) -> ++ %% Origin server issues a 301 to a second sim_http server (different port ++ %% = cross-origin). The target echoes which sensitive headers it received ++ %% back as x-received-* response headers so the test can inspect them. ++ OriginUrl = url(group_name(Config), "/301_custom_url.html", Config), ++ RequestOpts = proplists:get_value(request_opts, Config, []), ++ Profile = ?profile(Config), ++ ++ TargetPort = server_start(sim_http, []), ++ {ok, Host} = inet:gethostname(), ++ TargetUrl = ?URL_START ++ Host ++ ":" ++ integer_to_list(TargetPort) ++ ++ "/capture_sensitive_redirect_target.html", ++ ++ RedirectHeaders = [{"x-test-301-url", TargetUrl}, ++ {"authorization", "Basic dXNlcjpzM2NyM3Q="}, ++ {"proxy-authorization", "Basic dXNlcjpzM2NyM3Q="}, ++ {"cookie", "session=secret"}, ++ {"referer", "http://example.com/secret"}, ++ {"origin", "http://example.com"}], ++ ++ {ok, {{_, 200, _}, RespHeaders, _}} = ++ httpc:request(get, {OriginUrl, RedirectHeaders}, [?SSL_NO_VERIFY], RequestOpts, Profile), ++ ++ ?assertEqual("false", proplists:get_value("x-received-authorization", RespHeaders)), ++ ?assertEqual("false", proplists:get_value("x-received-proxy-authorization", RespHeaders)), ++ ?assertEqual("false", proplists:get_value("x-received-cookie", RespHeaders)), ++ ?assertEqual("false", proplists:get_value("x-received-referer", RespHeaders)), ++ ?assertEqual("false", proplists:get_value("x-received-origin", RespHeaders)). ++ ++%%------------------------------------------------------------------------- + cookie() -> + [{doc, "Test cookies on the default profile."}]. + cookie(Config) when is_list(Config) -> +@@ -3024,6 +3059,23 @@ handle_uri(_,"/delete_no_body.html", _,Headers,_, DefaultResponse) -> + not_found -> + DefaultResponse + end; ++%% Capture endpoint for redirect_strips_sensitive_headers. ++%% Echoes presence of each sensitive header as an x-received-* response header. ++handle_uri(_,"/capture_sensitive_redirect_target.html",_,Headers,_,_) -> ++ Present = fun(Name) -> ++ case proplists:is_defined(Name, Headers) of ++ true -> "true"; ++ false -> "false" ++ end ++ end, ++ "HTTP/1.1 200 OK\r\n" ++ ++ "Content-Length:0\r\n" ++ ++ "X-Received-Authorization:" ++ Present("authorization") ++ "\r\n" ++ ++ "X-Received-Proxy-Authorization:" ++ Present("proxy-authorization") ++ "\r\n" ++ ++ "X-Received-Cookie:" ++ Present("cookie") ++ "\r\n" ++ ++ "X-Received-Referer:" ++ Present("referer") ++ "\r\n" ++ ++ "X-Received-Origin:" ++ Present("origin") ++ "\r\n" ++ ++ "\r\n"; + handle_uri(_,_,_,_,_,DefaultResponse) -> + DefaultResponse. + +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48858.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48858.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48858.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48858.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,198 @@ +From b57c8d658246916518c8d99fa33b27d572f92198 Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Tue, 9 Jun 2026 16:30:27 +0200 +Subject: [PATCH] Merge branch 'ingela/maint-27/ftp/PASV/OTP-20166' into + maint-27 + +* ingela/maint-27/ftp/PASV/OTP-20166: + ftp: validate PASV response IP against control connection peer + +(cherry picked from commit 94686b01d4d3369d5b9f9e33f0433fbe79889a1c) +--- + lib/ftp/src/ftp_internal.erl | 5 +- + lib/ftp/test/ftp_SUITE.erl | 119 ++++++++++++++++++++++++++++++++++- + 2 files changed, 121 insertions(+), 3 deletions(-) + +diff --git a/lib/ftp/src/ftp_internal.erl b/lib/ftp/src/ftp_internal.erl +index 7989180e5a..6b612a66f6 100644 +--- a/lib/ftp/src/ftp_internal.erl ++++ b/lib/ftp/src/ftp_internal.erl +@@ -1505,6 +1505,7 @@ handle_ctrl_result({pos_compl, Lines}, + ipfamily = inet, + client = From, + caller = {setup_data_connection, Caller}, ++ csock = CSock, + timeout = Timeout, + sockopts_data_passive = SockOpts, + ftp_extension = false} = State) when is_list(Lines) -> +@@ -1513,10 +1514,10 @@ handle_ctrl_result({pos_compl, Lines}, + lists:splitwith(fun(?LEFT_PAREN) -> false; (_) -> true end, Lines), + {NewPortAddr, _} = + lists:splitwith(fun(?RIGHT_PAREN) -> false; (_) -> true end, Rest), +- [A1, A2, A3, A4, P1, P2] = ++ [_, _, _, _, P1, P2] = + lists:map(fun(X) -> list_to_integer(X) end, + string:tokens(NewPortAddr, [$,])), +- IP = {A1, A2, A3, A4}, ++ {ok, {IP, _}} = peername(CSock), + Port = (P1 * 256) + P2, + + ?DBG('<--data tcp connect to ~p:~p, Caller=~p~n',[IP,Port,Caller]), +diff --git a/lib/ftp/test/ftp_SUITE.erl b/lib/ftp/test/ftp_SUITE.erl +index c1284f6ff6..3a9a145af6 100644 +--- a/lib/ftp/test/ftp_SUITE.erl ++++ b/lib/ftp/test/ftp_SUITE.erl +@@ -63,7 +63,8 @@ all() -> + appup, + error_ehost, + error_datafail, +- clean_shutdown ++ clean_shutdown, ++ pasv_ip_not_validated + ]. + + groups() -> +@@ -317,6 +318,8 @@ init_per_testcase(Case, Config0) -> + clean_shutdown -> + Config = start_ftpd(Config0), + init_per_testcase2(Case, Config); ++ pasv_ip_not_validated -> ++ Config0; + _ -> + init_per_testcase2(Case, Config0) + end. +@@ -376,6 +379,7 @@ end_per_testcase(user, _Config) -> ok; + end_per_testcase(bad_user, _Config) -> ok; + end_per_testcase(error_elogin, _Config) -> ok; + end_per_testcase(error_ehost, _Config) -> ok; ++end_per_testcase(pasv_ip_not_validated, _Config) -> ok; + end_per_testcase(T, Config) when T =:= error_datafail; T =:= clean_shutdown -> + T == error_datafail andalso ftp__close(Config), + stop_ftpd(Config), +@@ -1099,6 +1103,69 @@ error_datafail(Config) -> + Result = Recv(Recv), + Result. + ++pasv_ip_not_validated() -> ++ [{doc, "PASV response IP must be validated against the control connection " ++ "peer address (CVE-2026-48858 / GHSA-24cv-hwgr-37fq). A malicious server " ++ "must not be able to redirect the data connection to an arbitrary host."}]. ++ ++pasv_ip_not_validated(_Config) -> ++ %% The victim service listens on 127.0.0.2 (a different loopback address). ++ %% The malicious FTP server listens on 127.0.0.1. ++ %% The PASV response will advertise 127.0.0.2:VictimPort. ++ %% Without the fix the client connects to 127.0.0.2 (victim). ++ %% With the fix the client ignores the IP in PASV and uses the control ++ %% peer address (127.0.0.1) instead, so the victim never gets a connection. ++ VictimIP = {127,0,0,2}, ++ {ok, VictimLSock} = gen_tcp:listen(0, ++ [binary, {reuseaddr, true}, {active, false}, inet, {ip, VictimIP}]), ++ {ok, VictimPort} = inet:port(VictimLSock), ++ ++ Self = self(), ++ spawn(fun() -> ++ case gen_tcp:accept(VictimLSock, 3000) of ++ {ok, Sock} -> ++ {ok, Peer} = inet:peername(Sock), ++ gen_tcp:close(Sock), ++ Self ! {victim_connected, Peer}; ++ {error, _} -> ++ Self ! victim_not_connected ++ end ++ end), ++ ++ %% Malicious FTP server on 127.0.0.1. ++ {ok, FtpLSock} = gen_tcp:listen(0, ++ [binary, {reuseaddr, true}, {active, false}, inet, {ip, {127,0,0,1}}]), ++ {ok, FtpPort} = inet:port(FtpLSock), ++ ++ spawn_link(fun() -> malicious_ftp_server(FtpLSock, {VictimIP, VictimPort}) end), ++ ++ application:ensure_started(ftp), ++ {ok, Pid} = ftp:open("127.0.0.1", [{port, FtpPort}]), ++ ok = ftp:user(Pid, "user", "pass"), ++ %% The ls call will trigger PASV. With the vulnerability present the ++ %% client connects to VictimPort; with the fix it should refuse to do so ++ %% and return an error instead. ++ _Ignored = ftp:ls(Pid), ++ catch ftp:close(Pid), ++ ++ Result = receive ++ {victim_connected, Peer} -> ++ {fail, Peer}; ++ victim_not_connected -> ++ ok ++ end, ++ ++ gen_tcp:close(FtpLSock), ++ gen_tcp:close(VictimLSock), ++ ++ case Result of ++ {fail, FailPeer} -> ++ ct:fail("ftp client connected data channel to redirected victim " ++ "address ~p instead of the FTP server (CVE-2026-48858)", ++ [FailPeer]); ++ ok -> ++ ok ++ end. + %%-------------------------------------------------------------------- + %% Internal functions ----------------------------------------------- + %%-------------------------------------------------------------------- +@@ -1410,3 +1477,53 @@ unwanted_error_report(LogFile) -> + ct:fail({no_logfile, LogFile}) + end. + ++%% Minimal FTP server that injects a malicious PASV redirect. ++malicious_ftp_server(LSock, VictimAddr) -> ++ {ok, Ctrl} = gen_tcp:accept(LSock), ++ gen_tcp:send(Ctrl, "220 PoC FTP Server\r\n"), ++ malicious_ftp_loop(Ctrl, VictimAddr). ++ ++malicious_ftp_loop(Ctrl, VictimAddr) -> ++ case malicious_ftp_recv_line(Ctrl) of ++ {ok, Line} -> ++ [Cmd | _] = string:tokens(string:trim(Line), " "), ++ malicious_ftp_handle(string:uppercase(Cmd), Ctrl, VictimAddr), ++ malicious_ftp_loop(Ctrl, VictimAddr); ++ {error, _} -> ++ gen_tcp:close(Ctrl) ++ end. ++ ++malicious_ftp_handle("USER", Ctrl, _) -> ++ gen_tcp:send(Ctrl, "331 Password required\r\n"); ++malicious_ftp_handle("PASS", Ctrl, _) -> ++ gen_tcp:send(Ctrl, "230 Logged in\r\n"); ++malicious_ftp_handle("SYST", Ctrl, _) -> ++ gen_tcp:send(Ctrl, "215 UNIX Type: L8\r\n"); ++malicious_ftp_handle("TYPE", Ctrl, _) -> ++ gen_tcp:send(Ctrl, "200 Type set\r\n"); ++malicious_ftp_handle("PASV", Ctrl, {{A1,A2,A3,A4}, VictimPort}) -> ++ %% Advertise the victim IP:port — a different host than the FTP server. ++ P1 = VictimPort bsr 8, ++ P2 = VictimPort band 16#FF, ++ Resp = io_lib:format( ++ "227 Entering Passive Mode (~b,~b,~b,~b,~b,~b)\r\n", ++ [A1, A2, A3, A4, P1, P2]), ++ gen_tcp:send(Ctrl, Resp); ++malicious_ftp_handle(Cmd, Ctrl, _) when Cmd =:= "LIST"; Cmd =:= "NLST" -> ++ gen_tcp:send(Ctrl, "150 Opening data connection\r\n"), ++ timer:sleep(200), ++ gen_tcp:send(Ctrl, "226 Transfer complete\r\n"); ++malicious_ftp_handle("QUIT", Ctrl, _) -> ++ gen_tcp:send(Ctrl, "221 Goodbye\r\n"), ++ gen_tcp:close(Ctrl); ++malicious_ftp_handle(_, Ctrl, _) -> ++ gen_tcp:send(Ctrl, "500 Unknown command\r\n"). ++ ++malicious_ftp_recv_line(Sock) -> ++ malicious_ftp_recv_line(Sock, <<>>). ++malicious_ftp_recv_line(Sock, Acc) -> ++ case gen_tcp:recv(Sock, 1, 5000) of ++ {ok, <<"\n">>} -> {ok, binary_to_list(<>)}; ++ {ok, Byte} -> malicious_ftp_recv_line(Sock, <>); ++ {error, _} = E -> E ++ end. +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48860.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48860.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48860.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-48860.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,29 @@ +From a8d8611cb33804a4936cdbe4b1f30d52dd804f9b Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Tue, 9 Jun 2026 16:30:26 +0200 +Subject: [PATCH] Merge branch 'ingela/ssl-dist/OTP-20156' into maint-27 + +* ingela/ssl-dist/OTP-20156: + ssl: Correct function used to check peer IP + +(cherry picked from commit f9bf5cfaad90afe100390c148be0711032b77a8b) +--- + lib/ssl/src/inet_tls_dist.erl | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/lib/ssl/src/inet_tls_dist.erl b/lib/ssl/src/inet_tls_dist.erl +index c3f1e02c27..9847b01bf9 100644 +--- a/lib/ssl/src/inet_tls_dist.erl ++++ b/lib/ssl/src/inet_tls_dist.erl +@@ -708,7 +708,7 @@ check_ip(Socket) -> + end, + {ok, Ifaddrs} ?= inet:getifaddrs(), + {ok, Netmask} ?= find_netmask(IP, Ifaddrs), +- {ok, {PeerIP, _}} ?= inet:sockname(Socket), ++ {ok, {PeerIP, _}} ?= inet:peername(Socket), + ok ?= if is_tuple(PeerIP) -> ok; + true -> {error, {no_ip_address, PeerIP}} + end, +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-49759.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-49759.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-49759.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-49759.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,217 @@ +From be63f13b5e042987f1dd21caaf75ed2669a96351 Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Tue, 9 Jun 2026 16:30:32 +0200 +Subject: [PATCH] Merge branch 'raimo/erts/inet_drv-sctp-error-oob/OTP-20165' + into maint-27 + +* raimo/erts/inet_drv-sctp-error-oob/OTP-20165: + Rewrite error parse loop to handle all lengths + Protect the output term buffer from overflow + +(cherry picked from commit cc60fddc5bbb6ff0261754ae3da84fda86e1ef62) +--- + erts/emulator/drivers/common/inet_drv.c | 103 +++++++++++++++--------- + 1 file changed, 64 insertions(+), 39 deletions(-) + +diff --git a/erts/emulator/drivers/common/inet_drv.c b/erts/emulator/drivers/common/inet_drv.c +index 3837b3e7ad..4a84ef8ed6 100644 +--- a/erts/emulator/drivers/common/inet_drv.c ++++ b/erts/emulator/drivers/common/inet_drv.c +@@ -3405,50 +3405,69 @@ static int sctp_parse_ancillary_data + ** concerns the protocol implementation), so we omit it: + */ + static int sctp_parse_error_chunk +- (ErlDrvTermData * spec, int i, char * chunk, int chlen) ++ (ErlDrvTermData * spec, int i, int spec_size, char * chunk, int chlen) + { + /* The "chunk" itself contains its length, which must not be greater than + the "chlen" derived from the over-all msg size: + */ +- char *causes, *cause; +- int coff, /* Cause offset */ +- ccode, /* Cause code */ +- clen, /* cause length */ +- s; +- int len = sock_ntohs (*((uint16_t*)(chunk+2))); +- ASSERT(len >= 4 && len <= chlen); +- +- causes = chunk + 4; +- coff = 0; +- len -= 4; /* Total length of the "causes" fields */ +- cause = causes; +- s = 0; +- +- while (coff < len) +- { ++ char *cause; /* Current cause pointer */ ++ int len; /* Remaining chunk length, chlen - (cause - chunk) */ ++ int s; /* List length */ ++ ++ s = 0; ++ len = sock_ntohs (*((uint16_t*)(chunk+2))); ++ len = MIN(len, chlen); ++ cause = chunk; ++ ++ /* There should be at least chunk header + one error code */ ++ if (len < 4+4) goto truncate; ++ /* Step over chunk header */ ++ len -= 4; ++ cause += 4; ++ ++ /* When len == 0 we have a clean end of Error causes */ ++ while (len > 0) { ++ int ccode, clen; /* Cause code, cause length */ ++ ++ if (len < 4) goto truncate; + ccode = sock_ntohs (*((uint16_t*)(cause))); + clen = sock_ntohs (*((uint16_t*)(cause + 2))); +- if (clen <= 0) +- /* Strange, but must guard against that! */ +- break; +- +- /* Install the corresp atom for this "ccode": */ +- i = LOAD_INT (spec, i, ccode); +- cause += clen; +- coff += clen; +- s ++; ++ if (clen < 4) goto truncate; ++ /* Install the corresp atom for this "ccode": */ ++ i = LOAD_INT (spec, i, ccode); s++; ++ if (i + 2*LOAD_INT_CNT+LOAD_NIL_CNT+LOAD_LIST_CNT > spec_size) { ++ /* We do not have room for two INT:s which is ++ * the worst case for the next iteration, so truncate now. ++ * ++ * We should have room for the truncation marker since ++ * we have added at most one INT after the previous check. ++ */ ++ goto truncate; ++ } ++ if (len < clen) goto truncate; ++ /* Step over Error cause */ ++ len -= clen; ++ cause += clen; + } +- i = LOAD_NIL (spec, i); +- i = LOAD_LIST(spec, i, s+1); ++ goto done; ++ ++ truncate: ++ /* Truncation marker - there is no error 0 */ ++ i = LOAD_INT (spec, i, 0); s++; ++ done: ++ /* Finalize the list */ ++ i = LOAD_NIL (spec, i); s++; ++ i = LOAD_LIST(spec, i, s); + return i; + } + ++ + /* + ** Parsing of SCTP notification events. NB: they are NOT ancillary data: they + ** are sent IN PLACE OF, not in conjunction with, the normal data: + */ + static int sctp_parse_async_event +- (ErlDrvTermData * spec, int i, int ok_pos, ++ (ErlDrvTermData * spec, int i, int spec_size, int ok_pos, + ErlDrvTermData error_atom, inet_descriptor* desc, + ErlDrvBinary * bin, int offs, int sz) + { +@@ -3582,7 +3601,7 @@ static int sctp_parse_async_event + + sizeof(sptr->sre_assoc_id); + # endif + chlen = sptr->sre_length - (chunk - (char *)sptr); +- i = sctp_parse_error_chunk(spec, i, chunk, chlen); ++ i = sctp_parse_error_chunk(spec, i, spec_size, chunk, chlen); + + i = LOAD_TUPLE (spec, i, 4); + /* The {error, {...}} will be closed by the caller */ +@@ -3822,6 +3841,7 @@ inet_async_binary_data + ErlDrvBinary * bin, int offs, int len, void *mp) + { + unsigned int hsz = desc->hsz + phsz; ++ const int spec_size = PACKET_ERL_DRV_TERM_DATA_LEN; + ErlDrvTermData spec [PACKET_ERL_DRV_TERM_DATA_LEN]; + ErlDrvTermData caller; + int aid; +@@ -3872,7 +3892,8 @@ inet_async_binary_data + condition; in the latter case, the 'ok' above is overridden by + an 'error', and the Event we receive contains the error term: */ + i = sctp_parse_async_event +- (spec, i, ok_pos, am_error, desc, bin, offs+hsz, sz); ++ (spec, i, spec_size - 3*LOAD_TUPLE_CNT, ++ ok_pos, am_error, desc, bin, offs+hsz, sz); + else + /* This is SCTP data, not a notification event. The data can be + returned as a List or as a Binary, similar to the generic case: +@@ -4056,6 +4077,7 @@ static int packet_binary_message(inet_descriptor* desc, + void *mp) + { + unsigned int hsz = desc->hsz; ++ const int spec_size = PACKET_ERL_DRV_TERM_DATA_LEN; + ErlDrvTermData spec [PACKET_ERL_DRV_TERM_DATA_LEN]; + int i = 0; + int alen; +@@ -4118,14 +4140,16 @@ static int packet_binary_message(inet_descriptor* desc, + i = sctp_parse_ancillary_data (spec, i, mptr); + + /* Then: Data or Event (Notification)? */ +- if (mptr->msg_flags & MSG_NOTIFICATION) ++ if (mptr->msg_flags & MSG_NOTIFICATION) { + /* This is an Event, parse it. It may indicate a normal or an error + condition; in the latter case, the initial 'sctp' atom is over- + ridden by 'sctp_error', and the Event we receive contains the + error term: */ + i = sctp_parse_async_event +- (spec, i, 0, am_sctp_error, desc, bin, offs, len); +- else ++ (spec, i, spec_size - 2*LOAD_TUPLE_CNT, ++ 0, am_sctp_error, desc, bin, offs, len); ++ } ++ else { + /* This is SCTP data, not a notification event. The data can be + returned as a List or as a Binary, similar to the generic case: + */ +@@ -4137,6 +4161,7 @@ static int packet_binary_message(inet_descriptor* desc, + else + /* INET_MODE_BINARY => Binary */ + i = LOAD_BINARY(spec, i, bin, offs, len); ++ } + + /* Close up the {[AncilData], Event_OR_Data} tuple: */ + i = LOAD_TUPLE (spec, i, 2); +@@ -9655,8 +9680,8 @@ static ErlDrvSSizeT sctp_fill_opts(inet_descriptor* desc, + int i = 0; + int length = 0; /* Number of result list entries */ + +- int spec_allocated = PACKET_ERL_DRV_TERM_DATA_LEN; +- spec = ALLOC(sizeof(* spec) * spec_allocated); ++ int spec_size = PACKET_ERL_DRV_TERM_DATA_LEN; ++ spec = ALLOC(sizeof(* spec) * spec_size); + + # define RETURN_ERROR(Spec, Errno) \ + do { \ +@@ -9668,7 +9693,7 @@ static ErlDrvSSizeT sctp_fill_opts(inet_descriptor* desc, + # define PLACE_FOR(Spec, Index, N) \ + do { \ + int need; \ +- if ((Index) > spec_allocated) { \ ++ if ((Index) > spec_size) { \ + erts_exit(ERTS_ERROR_EXIT,"Internal error in inet_drv, " \ + "miscalculated buffer size"); \ + } \ +@@ -9676,10 +9701,10 @@ static ErlDrvSSizeT sctp_fill_opts(inet_descriptor* desc, + if (need > INET_MAX_OPT_BUFFER/sizeof(ErlDrvTermData)) {\ + RETURN_ERROR((Spec), -ENOMEM); \ + } \ +- if (need > spec_allocated) { \ ++ if (need > spec_size) { \ + (Spec) = REALLOC((Spec), \ + sizeof(* (Spec)) \ +- * (spec_allocated = need + 20)); \ ++ * (spec_size = need + 20)); \ + } \ + } while (0) + +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-49760.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-49760.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-49760.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-49760.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,135 @@ +From 93afa8cfd8278ea6dc20de0bb024de48396b6b24 Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Tue, 9 Jun 2026 16:30:36 +0200 +Subject: [PATCH] Merge branch + 'sverker/erl_interface/big-print-overflow/OTP-20160' into maint-27 + +* sverker/erl_interface/big-print-overflow/OTP-20160: + erl_interface: Fix stack overflow in ei_s_print_term + +(cherry picked from commit 1465a643370c013137b9ddcf04240fcc67a478f5) +--- + lib/erl_interface/src/misc/ei_printterm.c | 13 +++-- + .../test/ei_print_SUITE_data/ei_print_test.c | 51 +++++++++++++------ + 2 files changed, 43 insertions(+), 21 deletions(-) + +diff --git a/lib/erl_interface/src/misc/ei_printterm.c b/lib/erl_interface/src/misc/ei_printterm.c +index 22c3325b10..1a8147577c 100644 +--- a/lib/erl_interface/src/misc/ei_printterm.c ++++ b/lib/erl_interface/src/misc/ei_printterm.c +@@ -58,12 +58,15 @@ static void xputc(char c, FILE* fp, ei_x_buff* x) + ei_x_append_buf(x, &c, 1); + } + +-static void xputs(const char* s, FILE* fp, ei_x_buff* x) ++static int xputs(const char* s, FILE* fp, ei_x_buff* x) + { ++ const int slen = strlen(s); ++ + if (fp != NULL) + fputs(s, fp); + else +- ei_x_append_buf(x, s, strlen(s)); ++ ei_x_append_buf(x, s, slen); ++ return slen; + } + + static int xprintf(FILE* fp, ei_x_buff* x, const char* fmt, ...) +@@ -74,9 +77,9 @@ static int xprintf(FILE* fp, ei_x_buff* x, const char* fmt, ...) + if (fp != NULL) { + r = vfprintf(fp, fmt, ap); + } else { +- /* FIXME always enough in buffer??? */ ++ /* We assume output is reasonable bounded */ + char tmpbuf[2000]; +- r = vsprintf(tmpbuf, fmt, ap); ++ r = vsnprintf(tmpbuf, sizeof(tmpbuf), fmt, ap); + ei_x_append_buf(x, tmpbuf, strlen(tmpbuf)); + } + va_end(ap); +@@ -353,7 +356,7 @@ static int print_term(FILE* fp, ei_x_buff* x, + goto err; + } + +- ch_written += xprintf(fp, x, ds); ++ ch_written += xputs(ds, fp, x); + free(ds); + ei_free_big(b); + +diff --git a/lib/erl_interface/test/ei_print_SUITE_data/ei_print_test.c b/lib/erl_interface/test/ei_print_SUITE_data/ei_print_test.c +index e0a3bb7c1c..0019e69637 100644 +--- a/lib/erl_interface/test/ei_print_SUITE_data/ei_print_test.c ++++ b/lib/erl_interface/test/ei_print_SUITE_data/ei_print_test.c +@@ -31,11 +31,12 @@ + static void + send_printed_buf(ei_x_buff* x) + { +- char* b = NULL; + char fn[256]; + char *tmp = getenv("temp"); +- FILE* f; +- int n, index = 0, ver; ++ FILE* f = NULL; ++ int n, n_s, index = 0, index_s, ver; ++ char* f_buf = NULL; ++ char* s_buf = NULL; + + if (tmp == NULL) { + tmp = "/tmp"; +@@ -44,22 +45,40 @@ send_printed_buf(ei_x_buff* x) + strcat(fn, "/ei_print_test.txt"); + f = fopen(fn, "w+"); + ei_decode_version(x->buff, &index, &ver); ++ index_s = index; + n = ei_print_term(f, x->buff, &index); +- if (n < 0) { +- fclose(f); +- x->index = 0; +- ei_x_format(x, "~s", "ERROR: term decoding failed"); +- send_bin_term(x); +- } else { ++ n_s = ei_s_print_term(&s_buf, x->buff, &index_s); ++ x->index = 0; ++ if (n != n_s) { ++ ei_x_format(x, "{~s,~i,~i}", ++ "ERROR: ei_print_term return values differ", ++ n, n_s); ++ } ++ else if (n < 0) { ++ ei_x_format(x, "{~s,~i}", "ERROR: term decoding failed", n); ++ } ++ else { + fseek(f, 0, SEEK_SET); +- b = malloc(n+1); +- fread(b, 1, n, f); +- b[n] = '\0'; ++ f_buf = malloc(n+1); ++ fread(f_buf, 1, n, f); ++ f_buf[n] = '\0'; ++ if (strcmp(f_buf, s_buf) != 0) { ++ ei_x_format(x, "{~s,~s,~s}", "ERROR: ei_print_term results differ", f_buf, s_buf); ++ } ++ else { ++ ei_x_format(x, "~s", f_buf); ++ } ++ } ++ send_bin_term(x); ++ ++ if (f) { + fclose(f); +- x->index = 0; +- ei_x_format(x, "~s", b); +- send_bin_term(x); +- free(b); ++ } ++ if (f_buf) { ++ free(f_buf); ++ } ++ if (s_buf) { ++ free(s_buf); + } + } + +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-53422.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-53422.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-53422.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-53422.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,449 @@ +From 90075523d2ec80e0c6215437dd894c18477ee7a0 Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Thu, 2 Jul 2026 10:02:18 +0200 +Subject: [PATCH] Merge branch + 'michal/maint-27/ssh/fix-sftp-realpath-filesystem-enumeration/OTP-20183' into + maint-27 + +* michal/maint-27/ssh/fix-sftp-realpath-filesystem-enumeration/OTP-20183: + Fix realpath path existence oracle + +(cherry picked from commit de8731f637e2976af210f0ad277490e4f14321c9) + +Debian note: conflicts in lib/ssh/src/ssh_sftpd.erl only because upstream +reindented the SSH_FXP_REALPATH clause and renamed Rlen to RLen in a commit +that is not backported. The conflict is resolved by keeping this tree's +formatting and variable name; the functional change is upstream's. +--- + lib/ssh/src/ssh_sftpd.erl | 23 ++- + lib/ssh/test/ssh_sftpd_SUITE.erl | 305 +++++++++++++++++++++++-------- + 2 files changed, 241 insertions(+), 87 deletions(-) + +diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl +index 0cdd0da62a..cdc946ea0c 100644 +--- a/lib/ssh/src/ssh_sftpd.erl ++++ b/lib/ssh/src/ssh_sftpd.erl +@@ -331,16 +331,21 @@ handle_op(?SSH_FXP_REALPATH, ReqId, + <>, + State0) -> + RelPath = relate_file_name(RPath, State0, _Canonicalize=false), +- {Res, State} = resolve_symlinks(RelPath, State0), ++ {Res, #state{root = Root} = State} = resolve_symlinks(RelPath, State0), + case Res of +- {ok, AbsPath} -> +- NewAbsPath = chroot_filename(AbsPath, State), +- XF = State#state.xf, +- Attr = #ssh_xfer_attr{type=directory}, +- ssh_xfer:xf_send_name(XF, ReqId, NewAbsPath, Attr), +- State; +- {error, _} = Error -> +- send_status(Error, ReqId, State) ++ {ok, AbsPath} -> ++ case Root =:= "" orelse is_within_root(Root, AbsPath) of ++ true -> ++ NewAbsPath = chroot_filename(AbsPath, State), ++ XF = State#state.xf, ++ Attr = #ssh_xfer_attr{type=directory}, ++ ssh_xfer:xf_send_name(XF, ReqId, NewAbsPath, Attr), ++ State; ++ false -> ++ send_status({error, enoent}, ReqId, State) ++ end; ++ {error, _} = Error -> ++ send_status(Error, ReqId, State) + end; + handle_op(?SSH_FXP_OPENDIR, ReqId, + <>, +diff --git a/lib/ssh/test/ssh_sftpd_SUITE.erl b/lib/ssh/test/ssh_sftpd_SUITE.erl +index 62505bbde2..e52401bcf3 100644 +--- a/lib/ssh/test/ssh_sftpd_SUITE.erl ++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl +@@ -45,6 +45,8 @@ + read_file/1, + max_path/1, + real_path/1, ++ real_path_root/1, ++ real_path_links_root/1, + relative_path/1, + relpath/1, + remove_file/1, +@@ -65,6 +67,14 @@ + -include("ssh.hrl"). + -include("ssh_test_lib.hrl"). + ++-record(link_test, {name, ++ link1, ++ link2, ++ expected ++ }). ++ ++-record(link, {location, target}). ++ + -define(USER, "Alladin"). + -define(PASSWD, "Sesame"). + %% -define(XFER_PACKET_SIZE, 32768). +@@ -73,7 +83,7 @@ + -define(REG_ATTERS, <<0,0,0,0,1>>). + -define(UNIX_EPOCH, 62167219200). + -define(MAX_HANDLES, 10). +--define(MAX_PATH, 200). ++-define(MAX_PATH, 220). + -define(is_set(F, Bits), ((F) band (Bits)) == (F)). + + %%-------------------------------------------------------------------- +@@ -94,6 +104,8 @@ all() -> + mk_rm_dir, + remove_file, + real_path, ++ real_path_root, ++ real_path_links_root, + retrieve_attributes, + set_attributes, + links, +@@ -142,104 +154,41 @@ end_per_group(_GroupName, Config) -> + + %%-------------------------------------------------------------------- + +-init_per_testcase(TestCase, Config) -> +- {OsFamily, _} = os:type(), ++init_per_testcase(TestCase, Config0) -> + ssh:start(), +- prep(Config), +- PrivDir = proplists:get_value(priv_dir, Config), ++ prep(Config0), ++ PrivDir = proplists:get_value(priv_dir, Config0), + ClientUserDir = filename:join(PrivDir, nopubkey), +- SystemDir = filename:join(proplists:get_value(priv_dir, Config), system), ++ SystemDir = filename:join(proplists:get_value(priv_dir, Config0), system), + + Options = [{system_dir, SystemDir}, +- {user_dir, PrivDir}, +- {user_passwords,[{?USER, ?PASSWD}]}, +- {pwdfun, fun(_,_) -> true end}], +- Result = case TestCase of +- ver6_basic -> +- SubSystems = [ssh_sftpd:subsystem_spec([{sftpd_vsn, 6}])], +- ssh:daemon(0, [{subsystems, SubSystems}|Options]); +- access_outside_root -> +- %% Build RootDir/access_outside_root/a/b and set Root and CWD +- BaseDir = filename:join(PrivDir, access_outside_root), +- RootDir = filename:join(BaseDir, a), +- CWD = filename:join(RootDir, b), +- %% Make the directory chain: +- ok = filelib:ensure_path(CWD), +- SubSystems = [ssh_sftpd:subsystem_spec([{root, RootDir}, +- {cwd, CWD}])], +- ssh:daemon(0, [{subsystems, SubSystems}|Options]); +- access_attributes_outside_root when OsFamily =:= win32 -> +- {skip, "Not implemented on windows"}; +- access_attributes_outside_root -> +- Rand = integer_to_list(rand:uniform(1000000)), +- RootDir = filename:join("/tmp", Rand), +- ok = file:make_dir(RootDir), +- SubSystems = [ssh_sftpd:subsystem_spec([{root, RootDir}])], +- ssh:daemon(0, [{subsystems, SubSystems}|Options]); +- root_with_cwd -> +- RootDir = filename:join(PrivDir, root_with_cwd), +- CWD = filename:join(RootDir, home), +- SubSystems = [ssh_sftpd:subsystem_spec([{root, RootDir}, {cwd, CWD}])], +- ssh:daemon(0, [{subsystems, SubSystems}|Options]); +- relative_path -> +- SubSystems = [ssh_sftpd:subsystem_spec([{cwd, PrivDir}])], +- ssh:daemon(0, [{subsystems, SubSystems}|Options]); +- open_file_dir_v5 -> +- SubSystems = [ssh_sftpd:subsystem_spec([{cwd, PrivDir}])], +- ssh:daemon(0, [{subsystems, SubSystems}|Options]); +- open_file_dir_v6 -> +- SubSystems = [ssh_sftpd:subsystem_spec([{cwd, PrivDir}, +- {sftpd_vsn, 6}])], +- ssh:daemon(0, [{subsystems, SubSystems}|Options]); +- links -> +- SubSystems = [ssh_sftpd:subsystem_spec([{cwd, PrivDir}])], +- ssh:daemon(0, [{subsystems, SubSystems}|Options]); +- links_root -> +- RootDir = filename:join(PrivDir, links_root), +- ok = file:make_dir(RootDir), +- SubSystems = [ssh_sftpd:subsystem_spec([{root, RootDir}, {cwd, RootDir}])], +- ssh:daemon(0, [{subsystems, SubSystems}|Options]); +- _ -> +- SubSystems = [ssh_sftpd:subsystem_spec( +- [{max_handles, ?MAX_HANDLES}, +- {max_path, ?MAX_PATH}])], +- ssh:daemon(0, [{subsystems, SubSystems}|Options]) +- end, +- +- case Result of +- {ok, Sftpd} -> ++ {user_dir, PrivDir}, ++ {user_passwords,[{?USER, ?PASSWD}]}, ++ {pwdfun, fun(_,_) -> true end}], ++ case prep_sftpd(TestCase, Config0) of ++ {{"sftp", _} = Spec, Config} -> ++ {ok, Sftpd} = ssh:daemon(0, [{subsystems, [Spec]} | Options]), + Port = ssh_test_lib:daemon_port(Sftpd), +- + Cm = ssh_test_lib:connect(Port, + [{user_dir, ClientUserDir}, + {user, ?USER}, {password, ?PASSWD}, + {user_interaction, false}, + {silently_accept_hosts, true}]), + {ok, Channel} = +- ssh_connection:session_channel(Cm, ?XFER_WINDOW_SIZE, +- ?XFER_PACKET_SIZE, ?SSH_TIMEOUT), +- ++ ssh_connection:session_channel(Cm, ?XFER_WINDOW_SIZE, ?XFER_PACKET_SIZE, ?SSH_TIMEOUT), + success = ssh_connection:subsystem(Cm, Channel, "sftp", ?SSH_TIMEOUT), +- + ProtocolVer = case atom_to_list(TestCase) of + "ver3_" ++ _ -> + 3; + _ -> + ?SSH_SFTP_PROTOCOL_VERSION + end, +- +- Data = <> , +- ++ Data = <>, + Size = 1 + size(Data), +- +- ssh_connection:send(Cm, Channel, << ?UINT32(Size), +- ?SSH_FXP_INIT, Data/binary >>), +- ++ ssh_connection:send(Cm, Channel, <>), + {ok, <>, _} + = reply(Cm, Channel), +- +- ct:log("Client: ~p Server ~p~n", [ProtocolVer, Version]), +- ++ ?CT_LOG("Client: ~p Server ~p~n", [ProtocolVer, Version]), + [{sftp, {Cm, Channel}}, {sftpd, Sftpd }| Config]; + Other -> + Other +@@ -254,6 +203,11 @@ end_per_testcase(access_attributes_outside_root, Config) -> + {_, {_, SftpdOpts}} = lists:keyfind("sftp", 1, Subsystems), + RootDir = proplists:get_value(root, SftpdOpts), + file:del_dir_r(RootDir); ++end_per_testcase(real_path_links_root, Config) -> ++ %% Workaround for buggy filelib:fold_files/5-6, we have to remove symlinks after test ++ LinkTests = proplists:get_value(link_tests, Config), ++ [delete_link_test(LinkTest, Config) || LinkTest <- LinkTests], ++ ssh_cleanup(Config); + end_per_testcase(_TestCase, Config) -> + ssh_cleanup(Config). + +@@ -536,6 +490,29 @@ real_path(Config) when is_list(Config) -> + true = RealPath == AbsPrivDir + end. + ++%%-------------------------------------------------------------------- ++real_path_root(Config) when is_list(Config) -> ++ {Cm, Channel} = proplists:get_value(sftp, Config), ++ Tests = ++ [{filename:join("/", below_root), "/below_root"}, ++ {filename:join("/", "."), "/"}, ++ {filename:join(["/", "..", above_root]),no_such_file}, ++ {"below_root", "/below_root"}, ++ {".", "/"}, ++ {filename:join("..", above_root), no_such_file}], ++ [verify_realpath(Cm, Channel, ReqId, Path, Expected) || ++ {ReqId, {Path, Expected}} <- lists:enumerate(0, Tests)], ++ ok. ++ ++%%-------------------------------------------------------------------- ++real_path_links_root(Config) -> ++ {Cm, Channel} = proplists:get_value(sftp, Config), ++ LinkTests = proplists:get_value(link_tests, Config), ++ [prep_link_test(LinkTest, Config) || LinkTest <- LinkTests], ++ [verify_realpath(Cm, Channel, ReqId, filename:join("/", Name), Expected) || ++ {ReqId, #link_test{name = Name, expected = Expected}} <- ++ lists:enumerate(0, LinkTests)]. ++ + %%-------------------------------------------------------------------- + links(Config) when is_list(Config) -> + case os:type() of +@@ -1005,6 +982,151 @@ prep(Config) -> + ok = file:write_file_info(TestFile, + FileInfo#file_info{mode = Mode}). + ++prep_sftpd(ver6_basic, Config) -> ++ {ssh_sftpd:subsystem_spec([{sftpd_vsn, 6}]), Config}; ++prep_sftpd(access_outside_root, Config) -> ++ PrivDir = proplists:get_value(priv_dir, Config), ++ %% Build RootDir/access_outside_root/a/b and set Root and CWD ++ BaseDir = filename:join(PrivDir, access_outside_root), ++ RootDir = filename:join(BaseDir, a), ++ CWD = filename:join(RootDir, b), ++ %% Make the directory chain: ++ ok = filelib:ensure_dir(filename:join(CWD, tmp)), ++ {ssh_sftpd:subsystem_spec([{root, RootDir}, {cwd, CWD}]), Config}; ++prep_sftpd(access_attributes_outside_root, Config) -> ++ case os:type() of ++ {win32, _} -> ++ {skip, "Not implemented on windows"}; ++ _ -> ++ Rand = integer_to_list(rand:uniform(1000000)), ++ RootDir = filename:join("/tmp", Rand), ++ ok = file:make_dir(RootDir), ++ {ssh_sftpd:subsystem_spec([{root, RootDir}]), Config} ++ end; ++prep_sftpd(root_with_cwd, Config) -> ++ PrivDir = proplists:get_value(priv_dir, Config), ++ RootDir = filename:join(PrivDir, root_with_cwd), ++ CWD = filename:join(RootDir, home), ++ {ssh_sftpd:subsystem_spec([{root, RootDir}, {cwd, CWD}]), Config}; ++prep_sftpd(TestCase, Config) when TestCase =:= relative_path; ++ TestCase =:= open_file_dir_v5; ++ TestCase =:= links -> ++ PrivDir = proplists:get_value(priv_dir, Config), ++ {ssh_sftpd:subsystem_spec([{cwd, PrivDir}]), Config}; ++prep_sftpd(open_file_dir_v6, Config) -> ++ PrivDir = proplists:get_value(priv_dir, Config), ++ {ssh_sftpd:subsystem_spec([{cwd, PrivDir}, {sftpd_vsn, 6}]), Config}; ++prep_sftpd(links_root, Config) -> ++ PrivDir = proplists:get_value(priv_dir, Config), ++ RootDir = filename:join(PrivDir, links_root), ++ ok = file:make_dir(RootDir), ++ {ssh_sftpd:subsystem_spec([{root, RootDir}, {cwd, RootDir}]), Config}; ++prep_sftpd(real_path_root, Config0) -> ++ case os:type() of ++ {win32, _} -> ++ {skip, "Not a relevant test on windows"}; ++ _ -> ++ PrivDir = proplists:get_value(priv_dir, Config0), ++ RootDir = filename:join(PrivDir, real_path_root), ++ ok = file:make_dir(RootDir), ++ BelowRootFile = filename:join(RootDir, below_root), ++ ok = file:write_file(BelowRootFile, <<>>), ++ AboveRootFile = filename:join(PrivDir, above_root), ++ ok = file:write_file(AboveRootFile, <<>>), ++ Config = [{root, RootDir}, {below_root, BelowRootFile}, {above_root, AboveRootFile} | Config0], ++ {ssh_sftpd:subsystem_spec([{root, RootDir}, {cwd, PrivDir}]), Config} ++ end; ++prep_sftpd(real_path_links_root = TestCase, Config0) -> ++ case os:type() of ++ {win32, _} -> ++ {skip, "Not a relevant test on windows"}; ++ _ -> ++ PrivDir = proplists:get_value(priv_dir, Config0), ++ RootDir = filename:join(PrivDir, TestCase), ++ ++ ok = file:make_dir(RootDir), ++ TargetAboveRoot = filename:join(PrivDir, target), ++ ok = file:write_file(TargetAboveRoot, <<>>), ++ TargetBelowRoot = filename:join(RootDir, target), ++ ok = file:write_file(TargetBelowRoot, <<>>), ++ ++ LinkTests = ++ [#link_test{name = abs_to_parent_of_root, ++ link1 = #link{target = [RootDir, ".."]}, ++ expected = no_such_file}, ++ #link_test{name = rel_to_parent_of_root, ++ link1 = #link{target = [".."]}, ++ expected = no_such_file}, ++ #link_test{name = abs_return_to_below_root, ++ link1 = #link{target = [TargetBelowRoot], ++ location = [PrivDir, abs_return_to_below_root]}, ++ link2 = #link{target = [PrivDir, abs_return_to_below_root]}, ++ expected = <<"/target">>}, ++ #link_test{name = rel_return_to_below_root, ++ link1 = #link{target = [".", TestCase, target], ++ location = [PrivDir, rel_return_to_below_root]}, ++ link2 = #link{target = ["..", rel_return_to_below_root]}, ++ expected = <<"/target">>}, ++ #link_test{name = abs_return_to_above_root, ++ link1 = #link{target = [TargetAboveRoot], ++ location = [PrivDir, abs_return_to_above_root]}, ++ link2 = #link{target = [PrivDir, abs_return_to_above_root]}, ++ expected = no_such_file}, ++ #link_test{name = rel_return_to_above_root, ++ link1 = #link{target = [".", target], ++ location = [PrivDir, rel_return_to_above_root]}, ++ link2 = #link{target = ["..", rel_return_to_above_root]}, ++ expected = no_such_file}, ++ #link_test{name = abs_escape_above_root, ++ link1 = #link{target = [RootDir, "..", target], ++ location = [PrivDir, abs_escape_above_root]}, ++ link2 = #link{target = [PrivDir, abs_escape_above_root]}, ++ expected = no_such_file}, ++ #link_test{name = rel_escape_above_root, ++ link1 = #link{target = [".", TestCase, "..", target], ++ location = [PrivDir, rel_escape_above_root]}, ++ link2 = #link{target = ["..", rel_escape_above_root]}, ++ expected = no_such_file}, ++ #link_test{name = abs_escape_above_root_and_back, ++ link1 = #link{target = [RootDir, "..", TestCase, target], ++ location = [PrivDir, abs_escape_above_root_and_back]}, ++ link2 = #link{target = [PrivDir, abs_escape_above_root_and_back]}, ++ expected = <<"/target">>}, ++ #link_test{name = rel_escape_above_root_and_back, ++ link1 = #link{target = [".", TestCase, "..", TestCase, target], ++ location = [PrivDir, rel_escape_above_root_and_back]}, ++ link2 = #link{target = ["..", rel_escape_above_root_and_back]}, ++ expected = <<"/target">>}], ++ Config = [{root, RootDir}, {link_tests, LinkTests} | Config0], ++ {ssh_sftpd:subsystem_spec([{root, RootDir}, {cwd, PrivDir}]), Config} ++ end; ++prep_sftpd(_TestCase, Config) -> ++ {ssh_sftpd:subsystem_spec([{max_handles, ?MAX_HANDLES}, {max_path, ?MAX_PATH}]), Config}. ++ ++prep_link_test(#link_test{name = Name, link1 = Link1, link2 = Link2}, Config) -> ++ ok = prep_link(Link1, Name, Config), ++ ok = prep_link(Link2, Name, Config). ++ ++prep_link(undefined, _Name, _Config) -> ++ ok; ++prep_link(#link{location = undefined, target = Target}, Name, Config) -> ++ RootDir = proplists:get_value(root, Config), ++ ok = file:make_symlink(filename:join(Target), filename:join(RootDir, Name)); ++prep_link(#link{location = Location, target = Target}, _Name, _Config) -> ++ ok = file:make_symlink(filename:join(Target), filename:join(Location)). ++ ++delete_link_test(#link_test{name = Name, link1 = Link1, link2 = Link2}, Config) -> ++ delete_link(Link1, Name, Config), ++ delete_link(Link2, Name, Config). ++ ++delete_link(undefined, _Name, _Config) -> ++ ok; ++delete_link(#link{location = undefined}, Name, Config) -> ++ RootDir = proplists:get_value(root, Config), ++ file:delete(filename:join(RootDir, Name)); ++delete_link(#link{location = Location}, _Name, _Config) -> ++ file:delete(filename:join(Location)). ++ + reply(Cm, Channel) -> + reply(Cm, Channel,<<>>). + +@@ -1280,3 +1402,30 @@ req_id() -> + end, + put(req_id, ReqId + 1), + ReqId. ++ ++ ++verify_realpath(Cm, Channel, ReqId, Path, no_such_file) -> ++ case real_path(Path, Cm, Channel, ReqId) of ++ {ok, <>, _} -> ++ ok; ++ {ok, <>, _} -> ++ ?CT_FAIL("Escape from root detected!~nPath: ~p~nExpected: SSH_FX_NO_SUCH_FILE~nActual: ~p~n", ++ [Path, ActualPath]); ++ Other -> ++ ?CT_FAIL("Unexpected response for path: ~p~nExpected: SSH_FX_NO_SUCH_FILE~nActual: ~p~n", ++ [Path, Other]) ++ end; ++verify_realpath(Cm, Channel, ReqId, Path, Expected) -> ++ ExpBin = iolist_to_binary(Expected), ++ case real_path(Path, Cm, Channel, ReqId) of ++ {ok, <>, _} -> ++ ok; ++ {ok, <>, _} -> ++ ?CT_FAIL("Unexpected status for path: ~p~nExpected: ~p~nActual: ~p~n", ++ [Path, Expected, Status]); ++ Other -> ++ ?CT_FAIL("Unexpected response for path: ~p~nExpected: ~p~nActual: ~p~n", ++ [Path, Expected, Other]) ++ end. +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54886.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54886.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54886.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54886.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,93 @@ +From 66e67bad8b545bb211f9bdb7358cde12b309611f Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Thu, 2 Jul 2026 10:02:19 +0200 +Subject: [PATCH] Merge branch + 'michal/ssh/fix-sftp-extended-data-infinite-loop/OTP-20186' into maint-27 + +* michal/ssh/fix-sftp-extended-data-infinite-loop/OTP-20186: + Fix extended data infinite loop in sftpd + Strip comments when installing ssh keys + Add ssh keys and license headers for ssh_sftpd_SUITE + +# Conflicts: +# lib/ssh/test/ssh_sftpd_SUITE.erl +# lib/ssh/test/ssh_test_lib.erl + +(cherry picked from commit 4e0bd6ba012010765660372d81be45f7aadc9ecb) +--- + lib/ssh/src/ssh_sftpd.erl | 9 ++++++--- + lib/ssh/test/ssh_sftpd_SUITE.erl | 17 +++++++++++++++-- + 2 files changed, 21 insertions(+), 5 deletions(-) + +diff --git a/lib/ssh/src/ssh_sftpd.erl b/lib/ssh/src/ssh_sftpd.erl +index cdc946ea0c..66537078a7 100644 +--- a/lib/ssh/src/ssh_sftpd.erl ++++ b/lib/ssh/src/ssh_sftpd.erl +@@ -262,7 +262,7 @@ handle_data(0, ChannelId, <>, + end; + handle_data(0, _ChannelId, Data, State = #state{pending = <<>>}) -> + {ok, State#state{pending = Data}}; +-handle_data(Type, ChannelId, Data0, State = #state{pending = Pending}) -> ++handle_data(0, ChannelId, Data0, State = #state{pending = Pending}) -> + Data = <>, + Size = byte_size(Data), + case Size > ?SSH_MAX_PACKET_SIZE of +@@ -283,8 +283,11 @@ handle_data(Type, ChannelId, Data0, State = #state{pending = Pending}) -> + ?LOG_ERROR(ReportFun, [Size]), + {stop, ChannelId, State}; + _ -> +- handle_data(Type, ChannelId, Data, State#state{pending = <<>>}) +- end. ++ handle_data(0, ChannelId, Data, State#state{pending = <<>>}) ++ end; ++handle_data(_Type, _ChannelId, _Data, State) -> ++ %% Same as openssh sftpd, we ignore extended data ++ {ok, State}. + + %% From draft-ietf-secsh-filexfer-02 "The file handle strings MUST NOT be longer than 256 bytes." + handle_op(Request, ReqId, <>, State = #state{xf = XF}) +diff --git a/lib/ssh/test/ssh_sftpd_SUITE.erl b/lib/ssh/test/ssh_sftpd_SUITE.erl +index e52401bcf3..60fea47ff7 100644 +--- a/lib/ssh/test/ssh_sftpd_SUITE.erl ++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl +@@ -58,7 +58,8 @@ + ver3_rename/1, + ver6_basic/1, + write_file/1, +- access_attributes_outside_root/1 ++ access_attributes_outside_root/1, ++ extended_data_no_infinite_loop/1 + ]). + + -include_lib("common_test/include/ct.hrl"). +@@ -119,7 +120,8 @@ all() -> + relative_path, + open_file_dir_v5, + open_file_dir_v6, +- access_attributes_outside_root]. ++ access_attributes_outside_root, ++ extended_data_no_infinite_loop]. + + groups() -> + []. +@@ -960,6 +962,17 @@ access_attributes_outside_root(Config) when is_list(Config) -> + file:delete(OutsideRootFile) + end. + ++%%-------------------------------------------------------------------- ++extended_data_no_infinite_loop(Config) when is_list(Config) -> ++ %% Regression test for CVE-2026-54886, sending extended data to ssh_sftpd.erl ++ %% caused an infinite loop ++ {Cm, Channel} = proplists:get_value(sftp, Config), ++ ok = ssh_connection:send(Cm, Channel, 1, <<"trigger">>), ++ ++ Data = <>, ++ ok = ssh_connection:send(Cm, Channel, Data), ++ {ok, <>, _} = reply(Cm, Channel). ++ + %%-------------------------------------------------------------------- + %% Internal functions ------------------------------------------------ + %%-------------------------------------------------------------------- +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54887.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54887.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54887.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54887.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,29 @@ +From 67bb75fbd96d932035259196b47e1489b8d3076a Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Thu, 2 Jul 2026 10:01:57 +0200 +Subject: [PATCH] Merge branch 'ingela/ssl/dtls-cookie/OTP-20194' into maint-27 + +* ingela/ssl/dtls-cookie/OTP-20194: + ssl: Avoid cookie forgery during setup up window + +(cherry picked from commit 07bf2a96099e84fe06e9ae1c6d5f2fee97311938) +--- + lib/ssl/src/dtls_server_connection.erl | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/lib/ssl/src/dtls_server_connection.erl b/lib/ssl/src/dtls_server_connection.erl +index acbd1107c8..7dbc82225e 100644 +--- a/lib/ssl/src/dtls_server_connection.erl ++++ b/lib/ssl/src/dtls_server_connection.erl +@@ -183,7 +183,7 @@ initial_hello(enter, _, State) -> + initial_hello({call, From}, {start, Timeout}, + #state{protocol_specific = PS0, recv = Recv} = State) -> + PS = PS0#{current_cookie_secret => dtls_v1:cookie_secret(), +- previous_cookie_secret => <<>>}, ++ previous_cookie_secret => dtls_v1:cookie_secret()}, + erlang:send_after(dtls_v1:cookie_timeout(), self(), new_cookie_secret), + dtls_gen_connection:next_event(hello, no_record, + State#state{recv = Recv#recv{from = From}, +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54890.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54890.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54890.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54890.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,52 @@ +From d6789907b3d38be073594511a8f5d0d47b796e4d Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Fri, 24 Jul 2026 17:04:13 +0200 +Subject: [PATCH] Merge branch 'john/erts/GHSA-54pw-5645-jh86/OTP-20259' into + maint-27 + +* john/erts/GHSA-54pw-5645-jh86/OTP-20259: + erts: Fix crash on decoding invalid ETF terms + +(cherry picked from commit c76f0dabbb0356f0f067a8773143dd4b76c6ca70) +--- + erts/emulator/beam/external.c | 8 ++++++-- + erts/emulator/test/binary_SUITE.erl | 4 ++++ + 2 files changed, 10 insertions(+), 2 deletions(-) + +diff --git a/erts/emulator/beam/external.c b/erts/emulator/beam/external.c +index d285528809..eed427a126 100644 +--- a/erts/emulator/beam/external.c ++++ b/erts/emulator/beam/external.c +@@ -4833,8 +4833,12 @@ dec_term_atom_common: + if (ep[-1] == BIT_BINARY_EXT) { + Uint trailing_bits = ep[4]; + +- if (((trailing_bits == 0) != (nu == 0)) || +- trailing_bits > 8) { ++ /* We accept a trailing bit count of 8 for backwards ++ * compatibility reasons, even though it's not the most ++ * compact representation. */ ++ if (trailing_bits < 1 || ++ trailing_bits > 8 || ++ size_in_bits < 8) { + goto error; + } + +diff --git a/erts/emulator/test/binary_SUITE.erl b/erts/emulator/test/binary_SUITE.erl +index ebf8c96bdc..d6052b3f53 100644 +--- a/erts/emulator/test/binary_SUITE.erl ++++ b/erts/emulator/test/binary_SUITE.erl +@@ -1160,6 +1160,10 @@ bad_binary_to_term(Config) when is_list(Config) -> + + %% Truncated UTF8 character (ERL-474) + bad_bin_to_term(<<131,119,1,194,163>>), ++ ++ %% Overlong 0-bit bitstring (should be encoded as 0-byte binary) ++ bad_bin_to_term(<<131,77,0,0,0,0,0>>), ++ + ok. + + bad_bin_to_term(BadBin) -> +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54891.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54891.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54891.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-54891.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,434 @@ +From 82545f3f81470a6ea5d8947d9a3ef9c7b26cbff7 Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Thu, 2 Jul 2026 10:02:00 +0200 +Subject: [PATCH] Merge branch 'ingela/ssl/client-guards/OTP-20207' into + maint-27 + +* ingela/ssl/client-guards/OTP-20207: + ssl: Refactor to keep state specific handling in state functions + ssl: TLS Client hardening + +(cherry picked from commit e07c1ba604856a994b533a5f3ed84e9463f06ebc) +--- + lib/ssl/src/dtls_client_connection.erl | 21 +++++++++++++++ + lib/ssl/src/dtls_server_connection.erl | 26 +++++++++++++++++- + lib/ssl/src/tls_client_connection.erl | 20 ++++++++++++++ + lib/ssl/src/tls_client_connection_1_3.erl | 6 +++++ + lib/ssl/src/tls_gen_connection.erl | 32 +---------------------- + lib/ssl/src/tls_server_connection.erl | 24 +++++++++++++++++ + lib/ssl/src/tls_server_connection_1_3.erl | 22 ++++++++++++++++ + 7 files changed, 119 insertions(+), 32 deletions(-) + +diff --git a/lib/ssl/src/dtls_client_connection.erl b/lib/ssl/src/dtls_client_connection.erl +index 0d2ac57b0a..750a65e008 100644 +--- a/lib/ssl/src/dtls_client_connection.erl ++++ b/lib/ssl/src/dtls_client_connection.erl +@@ -119,6 +119,7 @@ + + -include("dtls_connection.hrl"). + -include("dtls_handshake.hrl"). ++-include("dtls_record.hrl"). + -include("ssl_alert.hrl"). + -include("ssl_cipher.hrl"). + -include("ssl_internal.hrl"). +@@ -327,6 +328,10 @@ hello(internal, #change_cipher_spec{type = <<1>>}, State0) -> + end; + hello(state_timeout, Event, State) -> + dtls_gen_connection:handle_state_timeout(Event, ?STATE(hello), State); ++hello(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(hello), State); + hello(info, Event, State) -> + dtls_gen_connection:gen_info(Event, ?STATE(hello), State); + hello(Type, Event, State) -> +@@ -360,6 +365,10 @@ abbreviated(internal = Type, #change_cipher_spec{} = Event, + ConnectionStates = dtls_record:next_epoch(ConnectionStates1, read), + gen_state(?STATE(abbreviated), Type, Event, + State#state{connection_states = ConnectionStates}); ++abbreviated(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(abbreviated), State); + abbreviated(Type, Event, State) -> + gen_state(?STATE(abbreviated), Type, Event, State). + +@@ -372,6 +381,10 @@ wait_stapling(enter, _Event, State0) -> + {keep_state, State, Actions}; + wait_stapling(state_timeout, Event, State) -> + dtls_gen_connection:handle_state_timeout(Event, ?STATE(wait_stapling), State); ++wait_stapling(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(wait_stapling), State); + wait_stapling(info, Event, State) -> + dtls_gen_connection:gen_info(Event, ?STATE(wait_stapling), State); + wait_stapling(Type, Event, State) -> +@@ -403,6 +416,10 @@ certify(internal, #change_cipher_spec{type = <<1>>}, State0) -> + end; + certify(state_timeout, Event, State) -> + dtls_gen_connection:handle_state_timeout(Event, ?STATE(certify), State); ++certify(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(certify), State); + certify(info, Event, State) -> + dtls_gen_connection:gen_info(Event, ?STATE(certify), State); + certify(Type, Event, State) -> +@@ -440,6 +457,10 @@ cipher(internal = Type, #finished{} = Event, #state{connection_states = Connecti + catch throw:#alert{} = Alert -> + ssl_gen_statem:handle_own_alert(Alert, cipher, State) + end; ++cipher(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(cipher), State); + cipher(Type, Event, State) -> + gen_state(?STATE(cipher), Type, Event, State). + +diff --git a/lib/ssl/src/dtls_server_connection.erl b/lib/ssl/src/dtls_server_connection.erl +index 7dbc82225e..2a42aeaaa5 100644 +--- a/lib/ssl/src/dtls_server_connection.erl ++++ b/lib/ssl/src/dtls_server_connection.erl +@@ -119,6 +119,7 @@ + + -include("dtls_connection.hrl"). + -include("dtls_handshake.hrl"). ++-include("dtls_record.hrl"). + -include("ssl_alert.hrl"). + -include("ssl_cipher.hrl"). + -include("ssl_internal.hrl"). +@@ -203,6 +204,10 @@ initial_hello({call, From}, {start, {Opts, EmOpts}, Timeout}, + catch throw:Error -> + {stop_and_reply, {shutdown, normal}, {reply, From, {error, Error}}, State0} + end; ++initial_hello(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(initial_hello), State); + initial_hello(Type, Event, State) -> + tls_dtls_server_connection:initial_hello(Type, Event, State). + +@@ -299,7 +304,10 @@ hello(internal, #change_cipher_spec{type = <<1>>}, State0) -> + {stop, _, _} = Stop -> + Stop + end; +- ++hello(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(hello), State); + hello(info, Event, State) -> + dtls_gen_connection:gen_info(Event, ?STATE(hello), State); + hello(state_timeout, Event, State) -> +@@ -335,6 +343,10 @@ abbreviated(internal = Type, #change_cipher_spec{} = Event, + ConnectionStates = dtls_record:next_epoch(ConnectionStates1, read), + gen_state(?STATE(abbreviated), Type, Event, + State#state{connection_states = ConnectionStates}); ++abbreviated(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(abbreviated), State); + abbreviated(Type, Event, State) -> + gen_state(?STATE(abbreviated), Type, Event, State). + +@@ -360,6 +372,10 @@ certify(internal, #change_cipher_spec{type = <<1>>}, State0) -> + end; + certify(state_timeout, Event, State) -> + dtls_gen_connection:handle_state_timeout(Event, ?STATE(certify), State); ++certify(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(certify), State); + certify(info, Event, State) -> + dtls_gen_connection:gen_info(Event, ?STATE(certify), State); + certify(Type, Event, State) -> +@@ -376,6 +392,10 @@ wait_cert_verify(state_timeout, Event, State) -> + dtls_gen_connection:handle_state_timeout(Event, ?STATE(wait_cert_verify), State); + wait_cert_verify(info, Event, State) -> + dtls_gen_connection:gen_info(Event, ?STATE(wait_cert_verify), State); ++wait_cert_verify(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(wait_cert_verify), State); + wait_cert_verify(Type, Event, State) -> + gen_state(?STATE(wait_cert_verify), Type, Event, State). + +@@ -402,6 +422,10 @@ cipher(internal = Type, #finished{} = Event, #state{connection_states = Connecti + State#state{connection_states = ConnectionStates, + protocol_specific = + PS#{flight_state => connection}})); ++cipher(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(cipher), State); + cipher(Type, Event, State) -> + gen_state(?STATE(cipher), Type, Event, State). + +diff --git a/lib/ssl/src/tls_client_connection.erl b/lib/ssl/src/tls_client_connection.erl +index c99226f78e..77de85ae31 100644 +--- a/lib/ssl/src/tls_client_connection.erl ++++ b/lib/ssl/src/tls_client_connection.erl +@@ -327,6 +327,10 @@ hello(internal, #server_hello{} = Hello, + catch throw:#alert{} = Alert -> + ssl_gen_statem:handle_own_alert(Alert, ?STATE(hello), State) + end; ++hello(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(hello), State); + hello(info, Event, State) -> + tls_gen_connection:gen_info(Event, ?STATE(hello), State); + hello(Type, Event, State) -> +@@ -343,6 +347,10 @@ user_hello(Type, Event, State) -> + -spec abbreviated(gen_statem:event_type(), term(), #state{}) -> + gen_statem:state_function_result(). + %%-------------------------------------------------------------------- ++abbreviated(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(abbreviated), State); + abbreviated(info, Event, State) -> + tls_gen_connection:gen_info(Event, ?STATE(abbreviated), State); + abbreviated(Type, Event, State) -> +@@ -352,6 +360,10 @@ abbreviated(Type, Event, State) -> + -spec wait_stapling(gen_statem:event_type(), term(), #state{}) -> + gen_statem:state_function_result(). + %%-------------------------------------------------------------------- ++wait_stapling(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(wait_stapling), State); + wait_stapling(info, Event, State) -> + tls_gen_connection:gen_info(Event, ?STATE(wait_stapling), State); + wait_stapling(Type, Event, State) -> +@@ -361,6 +373,10 @@ wait_stapling(Type, Event, State) -> + -spec certify(gen_statem:event_type(), term(), #state{}) -> + gen_statem:state_function_result(). + %%-------------------------------------------------------------------- ++certify(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(certify), State); + certify(info, Event, State) -> + tls_gen_connection:gen_info(Event, ?STATE(certify), State); + certify(Type, Event, State) -> +@@ -370,6 +386,10 @@ certify(Type, Event, State) -> + -spec cipher(gen_statem:event_type(), term(), #state{}) -> + gen_statem:state_function_result(). + %%-------------------------------------------------------------------- ++cipher(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(cipher), State); + cipher(info, Event, State) -> + tls_gen_connection:gen_info(Event, ?STATE(cipher), State); + cipher(Type, Event, State) -> +diff --git a/lib/ssl/src/tls_client_connection_1_3.erl b/lib/ssl/src/tls_client_connection_1_3.erl +index 2ad54636d9..a4679c218f 100644 +--- a/lib/ssl/src/tls_client_connection_1_3.erl ++++ b/lib/ssl/src/tls_client_connection_1_3.erl +@@ -247,6 +247,9 @@ start(internal, #server_hello{}, State0) -> + %%so it is a previous version hello. + ssl_gen_statem:handle_own_alert( + ?ALERT_REC(?FATAL, ?PROTOCOL_VERSION), ?STATE(start), State0); ++start(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(start), State); + start(info, Msg, State) -> + tls_gen_connection:gen_info(Msg, ?STATE(start), State); + start(Type, Msg, State) -> +@@ -299,6 +302,9 @@ wait_sh(internal, #server_hello{} = Hello, + tls_gen_connection:next_event(hello_middlebox_assert, + no_record, State1) + end; ++wait_sh(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(wait_sh), State); + wait_sh(info, Msg, State) -> + tls_gen_connection:gen_info(Msg, ?STATE(wait_sh), State); + wait_sh(Type, Msg, State) -> +diff --git a/lib/ssl/src/tls_gen_connection.erl b/lib/ssl/src/tls_gen_connection.erl +index cb2b4ea6da..fe87aebfce 100644 +--- a/lib/ssl/src/tls_gen_connection.erl ++++ b/lib/ssl/src/tls_gen_connection.erl +@@ -400,37 +400,7 @@ next_event(StateName, #ssl_tls{} = Record, State, Actions) -> + next_event(StateName, #alert{} = Alert, State, Actions) -> + {next_state, StateName, State, [{next_event, internal, Alert} | Actions]}. + +-%%% TLS record protocol level application data messages +-handle_protocol_record(#ssl_tls{type = ?APPLICATION_DATA}, StateName, +- #state{static_env = #static_env{role = server}, +- handshake_env = #handshake_env{renegotiation = {false, first}} +- } = State) when StateName == initial_hello; +- StateName == hello; +- StateName == certify; +- StateName == wait_cert_verify; +- StateName == wait_stapling; +- StateName == abbreviated; +- StateName == cipher +- -> +- %% Application data can not be sent before initial handshake pre TLS-1.3. +- Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), +- ssl_gen_statem:handle_own_alert(Alert, StateName, State); +-handle_protocol_record(#ssl_tls{type = ?APPLICATION_DATA, early_data = false}, StateName, +- #state{static_env = #static_env{role = server} +- } = State) when StateName == start; +- StateName == recvd_ch; +- StateName == negotiated; +- StateName == wait_eoed -> +- Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, none_early_application_data_before_handshake), +- ssl_gen_statem:handle_own_alert(Alert, StateName, State); +-handle_protocol_record(#ssl_tls{type = ?APPLICATION_DATA}, StateName, +- #state{static_env = #static_env{role = server} +- } = State) when StateName == wait_cert; +- StateName == wait_cv; +- StateName == wait_finished-> +- Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, +- application_data_before_handshake_or_intervened_in_post_handshake_auth), +- ssl_gen_statem:handle_own_alert(Alert, StateName, State); ++%%% TLS record protocol level application data messages + handle_protocol_record(#ssl_tls{type = ?APPLICATION_DATA, fragment = Data}, StateName, + #state{recv = #recv{from = From}, + socket_options = #socket_options{active = false}} = State0) +diff --git a/lib/ssl/src/tls_server_connection.erl b/lib/ssl/src/tls_server_connection.erl +index b755a0dad8..d922c8f823 100644 +--- a/lib/ssl/src/tls_server_connection.erl ++++ b/lib/ssl/src/tls_server_connection.erl +@@ -193,6 +193,10 @@ initial_hello({call, From}, {start, {Opts, EmOpts}, Timeout}, + catch throw:Error -> + {stop_and_reply, {shutdown, normal}, {reply, From, {error, Error}}, State0} + end; ++initial_hello(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(initial_hello), State); + initial_hello(Type, Event, State) -> + tls_dtls_server_connection:initial_hello(Type, Event, State). + +@@ -236,6 +240,10 @@ hello(internal, #client_hello{client_version = ClientVersion} = Hello, + State0#state{connection_env = NewCenv}, + ssl_gen_statem:handle_own_alert(Alert, ?STATE(hello), AlertState) + end; ++hello(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(hello), State); + hello(info, Event, State) -> + tls_gen_connection:gen_info(Event, ?STATE(hello), State); + hello(Type, Event, State) -> +@@ -254,6 +262,10 @@ user_hello(Type, Event, State) -> + %%-------------------------------------------------------------------- + abbreviated(info, Event, State) -> + tls_gen_connection:gen_info(Event, ?STATE(abbreviated), State); ++abbreviated(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(abbreviated), State); + abbreviated(Type, Event, State) -> + gen_state(?STATE(abbreviated), Type, Event, State). + +@@ -263,6 +275,10 @@ abbreviated(Type, Event, State) -> + %%-------------------------------------------------------------------- + certify(info, Event, State) -> + tls_gen_connection:gen_info(Event, ?STATE(certify), State); ++certify(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(certify), State); + certify(Type, Event, State) -> + gen_state(?STATE(certify), Type, Event, State). + +@@ -297,6 +313,10 @@ wait_cert_verify(internal, #certificate_verify{signature = Signature, + #alert{} = Alert -> + throw(Alert) + end; ++wait_cert_verify(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(wait_cert_verify), State); + wait_cert_verify(Type, Event, State) -> + ssl_gen_statem:handle_common_event(Type, Event, ?STATE(wait_cert_verify), State). + +@@ -306,6 +326,10 @@ wait_cert_verify(Type, Event, State) -> + %%-------------------------------------------------------------------- + cipher(info, Event, State) -> + tls_gen_connection:gen_info(Event, ?STATE(cipher), State); ++cipher(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, ++ #state{handshake_env = #handshake_env{renegotiation = {false, first}}} = State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, application_data_before_initial_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(cipher), State); + cipher(Type, Event, State) -> + gen_state(?STATE(cipher), Type, Event, State). + +diff --git a/lib/ssl/src/tls_server_connection_1_3.erl b/lib/ssl/src/tls_server_connection_1_3.erl +index 8f0e0864c0..a044585d54 100644 +--- a/lib/ssl/src/tls_server_connection_1_3.erl ++++ b/lib/ssl/src/tls_server_connection_1_3.erl +@@ -235,6 +235,9 @@ start(internal, #client_hello{} = Hello, + start(internal, #client_hello{}, State0) -> %% Missing mandantory TLS-1.3 extensions, + %% so it is a previous version hello. + ssl_gen_statem:handle_own_alert(?ALERT_REC(?FATAL, ?PROTOCOL_VERSION), ?STATE(start), State0); ++start(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA, early_data = false}}, State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, none_early_application_data_before_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(start), State); + start(info, Msg, State) -> + tls_gen_connection:gen_info(Msg, ?STATE(start), State); + start(Type, Msg, State) -> +@@ -258,6 +261,10 @@ negotiated(internal, {start_handshake, _} = Message, State0) -> + {State, NextState} -> + {next_state, NextState, State, []} + end; ++negotiated(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA, early_data = false}}, ++ State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, none_early_application_data_before_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(negotiated), State); + negotiated(info, Msg, State) -> + tls_gen_connection:gen_info(Msg, ?STATE(negotiated), State); + negotiated(Type, Msg, State) -> +@@ -268,6 +275,10 @@ negotiated(Type, Msg, State) -> + {start, timeout()} | term(), #state{}) -> + gen_statem:state_function_result(). + %%-------------------------------------------------------------------- ++wait_cert(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, ++ application_data_before_handshake_or_intervened_in_post_handshake_auth), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(wait_cert), State); + wait_cert(Type, Msg, State) -> + tls_gen_connection_1_3:wait_cert(Type, Msg, State). + +@@ -289,6 +300,10 @@ wait_cv(internal, + {Ref, {#alert{} = Alert, AState}} -> + ssl_gen_statem:handle_own_alert(Alert, ?STATE(wait_cv), AState) + end; ++wait_cv(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, ++ application_data_before_handshake_or_intervened_in_post_handshake_auth), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(wait_cv), State); + wait_cv(Type, Msg, State) -> + tls_gen_connection_1_3:wait_cv(Type, Msg, State). + +@@ -326,6 +341,10 @@ wait_finished(internal, + {Ref, #alert{} = Alert} -> + ssl_gen_statem:handle_own_alert(Alert, ?STATE(wait_finished), State0) + end; ++wait_finished(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA}}, State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, ++ application_data_before_handshake_or_intervened_in_post_handshake_auth), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(wait_finished), State); + wait_finished(info, Msg, State) -> + tls_gen_connection:gen_info(Msg, ?STATE(wait_finished), State); + wait_finished(Type, Msg, State) -> +@@ -352,6 +371,9 @@ wait_eoed(internal, #end_of_early_data{}, #state{handshake_env = HsEnv0} = State + ssl_gen_statem:handle_own_alert(?ALERT_REC(?FATAL, ?INTERNAL_ERROR, Reason), + wait_eoed, State0) + end; ++wait_eoed(internal, {protocol_record, #ssl_tls{type = ?APPLICATION_DATA, early_data = false}}, State) -> ++ Alert = ?ALERT_REC(?FATAL, ?UNEXPECTED_MESSAGE, none_early_application_data_before_handshake), ++ ssl_gen_statem:handle_own_alert(Alert, ?STATE(wait_eoed), State); + wait_eoed(info, Msg, State) -> + tls_gen_connection:gen_info(Msg, ?STATE(wait_eoed), State); + wait_eoed(Type, Msg, State) -> +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55737.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55737.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55737.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55737.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,47 @@ +From 8df7673b8666931bd99ea86cdf45708cdc6d04f5 Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Fri, 24 Jul 2026 17:04:09 +0200 +Subject: [PATCH] Merge branch + 'kiko/erts/fixes-heap-pointer-signed-unsigned/OTP-20214' into maint-27 + +* kiko/erts/fixes-heap-pointer-signed-unsigned/OTP-20214: + erts: Fixes heap pointer corruption via signed/unsigned mismatch + +(cherry picked from commit 2b18cb1d0b139d9f24667c9b5b92a4fc13b7004e) +--- + erts/emulator/beam/external.c | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/erts/emulator/beam/external.c b/erts/emulator/beam/external.c +index 3fee55cb1c..d285528809 100644 +--- a/erts/emulator/beam/external.c ++++ b/erts/emulator/beam/external.c +@@ -34,6 +34,7 @@ + #include "sys.h" + #include "erl_vm.h" + #include "global.h" ++#include "erl_term.h" + #include "erl_process.h" + #include "error.h" + #include "external.h" +@@ -4480,6 +4481,7 @@ dec_term_atom_common: + break; + } + tuple_loop: ++ ASSERT(n > 0 && n <= ERTS_MAX_TUPLE_SIZE); + *objp = make_tuple(hp); + *hp++ = make_arityval(n); + hp += n; +@@ -6041,6 +6043,9 @@ init_done: + case LARGE_TUPLE_EXT: + CHKSIZE(4); + n = get_uint32(ep); ++ if (n > ERTS_MAX_TUPLE_SIZE) { ++ goto error; ++ } + ep += 4; + CHKSIZE(n); /* Fail faster if the binary is too short. */ + ADDTERMS(n); +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55950.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55950.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55950.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55950.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,38 @@ +From e84ae26220a0a4e4843f91f222b458517e97e0c5 Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Thu, 2 Jul 2026 10:02:17 +0200 +Subject: [PATCH] Merge branch 'ingela/dtls/OTP-20220' into maint-27 + +* ingela/dtls/OTP-20220: + ssl: Fix DTLS race condition + +(cherry picked from commit e9b49dd8029f096c6c7091fd6c6b43f592b8a14a) +--- + lib/ssl/src/dtls_packet_demux.erl | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/lib/ssl/src/dtls_packet_demux.erl b/lib/ssl/src/dtls_packet_demux.erl +index 97ba511f4c..de12581307 100644 +--- a/lib/ssl/src/dtls_packet_demux.erl ++++ b/lib/ssl/src/dtls_packet_demux.erl +@@ -162,7 +162,7 @@ handle_call({new_connection, Old, _Pid}, _, + case kv_lookup(Old, MsgQs0) of + {value, OldQueue} -> + MsgQs1 = kv_delete(Old, MsgQs0), +- MsgQs = kv_insert({old,Old}, OldQueue, MsgQs1), ++ MsgQs = kv_enter({old,Old}, OldQueue, MsgQs1), + {reply, true, State#state{dtls_msq_queues = MsgQs}}; + none -> + %% Already set as old +@@ -365,6 +365,8 @@ kv_lookup(Key, Store) -> + gb_trees:lookup(Key, Store). + kv_insert(Key, Value, Store) -> + gb_trees:insert(Key, Value, Store). ++kv_enter(Key, Value, Store) -> ++ gb_trees:enter(Key, Value, Store). + kv_get(Key, Store) -> + gb_trees:get(Key, Store). + kv_delete(Key, Store) -> +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55952.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55952.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55952.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55952.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,35 @@ +From 24e178f80869bd6c7835e3f61786e209d5a5182d Mon Sep 17 00:00:00 2001 +From: Ingela Anderton Andin +Date: Tue, 23 Jun 2026 17:58:22 +0200 +Subject: [PATCH] ssl: Add PSK parameter check + +(cherry picked from commit 9b5437c72fa3403a75c1aba28e5c532bc191c662) +--- + lib/ssl/src/tls_handshake_1_3.erl | 11 ++++++++++- + 1 file changed, 10 insertions(+), 1 deletion(-) + +diff --git a/lib/ssl/src/tls_handshake_1_3.erl b/lib/ssl/src/tls_handshake_1_3.erl +index f1c03ba46a..f96e72f69f 100644 +--- a/lib/ssl/src/tls_handshake_1_3.erl ++++ b/lib/ssl/src/tls_handshake_1_3.erl +@@ -1701,7 +1701,16 @@ handle_pre_shared_key(#state{ssl_options = #{session_tickets := Tickets}, + OfferedPreSharedKeys}, Cipher) when Tickets =/= disabled -> + Tracker = proplists:get_value(session_tickets_tracker, Trackers), + #{prf := CipherHash} = ssl_cipher_format:suite_bin_to_map(Cipher), +- tls_server_session_ticket:use(Tracker, OfferedPreSharedKeys, CipherHash, HHistory). ++ #offered_psks{ ++ identities = Identities, ++ binders = Binders ++ } = OfferedPreSharedKeys, ++ case length(Identities) == length(Binders) of ++ true -> ++ tls_server_session_ticket:use(Tracker, OfferedPreSharedKeys, CipherHash, HHistory); ++ false -> ++ {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, illegal_pre_shared_key)} ++ end. + + %% If the handshake includes a HelloRetryRequest, the initial + %% ClientHello and HelloRetryRequest are included in the transcript +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55953.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55953.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55953.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55953.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,383 @@ +From e7f1751fbfb8d355dcea356e6e48e406e92dd2af Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Fri, 24 Jul 2026 17:04:09 +0200 +Subject: [PATCH] Merge branch + 'ingela/maint-27/ssl/pre-13-client-cipher-suite-validation/OTP-20240' into + maint-27 + +* ingela/maint-27/ssl/pre-13-client-cipher-suite-validation/OTP-20240: + ssl: Add test that client rejects unofferd algorithms + ssl: Add pre TLS-1.3 client alpn check + ssl: Add pre TLS-1.3 client cipher suite check + +(cherry picked from commit 1213c752de9f39501faeefaf5d67c70255ad194d) +--- + lib/ssl/src/ssl_handshake.erl | 117 +++++++++++++---------- + lib/ssl/test/tls_api_SUITE.erl | 170 ++++++++++++++++++++++++++++++++- + 2 files changed, 235 insertions(+), 52 deletions(-) + +diff --git a/lib/ssl/src/ssl_handshake.erl b/lib/ssl/src/ssl_handshake.erl +index 94c0b7487d..8dcb2578d4 100644 +--- a/lib/ssl/src/ssl_handshake.erl ++++ b/lib/ssl/src/ssl_handshake.erl +@@ -1548,53 +1548,38 @@ handle_server_hello_extensions(RecordCB, Random, CipherSuite, + #{secure_renegotiate := SecureRenegotation} = + SslOpts, + ConnectionStates0, Renegotiation, IsNew) -> +- ConnectionStates = handle_renegotiation_extension(client, RecordCB, Version, +- maps:get(renegotiation_info, Exts, undefined), Random, ++ AvailableCipherSuites = available_suites(maps:get(ciphers, SslOpts), Version), ++ validate_cipher_suite(CipherSuite, AvailableCipherSuites), ++ ConnectionStates = handle_renegotiation_extension(client, RecordCB, Version, ++ maps:get(renegotiation_info, Exts, undefined), Random, + CipherSuite, undefined, + ConnectionStates0, + Renegotiation, SecureRenegotation), +- +- %% RFC 6066: handle received/expected maximum fragment length +- if IsNew -> +- ServerMaxFragEnum = maps:get(max_frag_enum, Exts, undefined), +- ConnMaxFragLen = maps:get(max_fragment_length, ConnectionStates0, undefined), +- ClientMaxFragEnum = max_frag_enum(ConnMaxFragLen), +- +- if ServerMaxFragEnum == ClientMaxFragEnum -> +- ok; +- true -> +- throw(?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER)) +- end; +- true -> +- ok +- end, +- +- case handle_cert_status_extension(SslOpts, Exts) of +- #alert{} = Alert -> +- Alert; +- StaplingState -> +- %% If we receive an ALPN extension then this is the protocol selected, +- %% otherwise handle the NPN extension. +- ALPN = maps:get(alpn, Exts, undefined), +- case decode_alpn(ALPN) of +- %% ServerHello contains exactly one protocol: the one selected. +- %% We also ignore the ALPN extension during renegotiation (see encode_alpn/2). +- [Protocol] when not Renegotiation -> +- {ConnectionStates, alpn, Protocol, StaplingState}; +- [_] when Renegotiation -> +- {ConnectionStates, alpn, undefined, StaplingState}; +- undefined -> +- NextProtocolNegotiation = maps:get(next_protocol_negotiation, Exts, undefined), +- NextProtocolSelector = maps:get(next_protocol_selector, SslOpts, undefined), +- Protocol = handle_next_protocol(NextProtocolNegotiation, NextProtocolSelector, Renegotiation), +- {ConnectionStates, npn, Protocol, StaplingState}; +- {error, Reason} -> +- ?ALERT_REC(?FATAL, ?HANDSHAKE_FAILURE, Reason); +- [] -> +- ?ALERT_REC(?FATAL, ?HANDSHAKE_FAILURE, no_protocols_in_server_hello); +- [_|_] -> +- ?ALERT_REC(?FATAL, ?HANDSHAKE_FAILURE, too_many_protocols_in_server_hello) +- end ++ assert_max_frag_length(IsNew, Exts, ConnectionStates0), ++ StaplingState = handle_cert_status_extension(SslOpts, Exts), ++ %% If we receive an ALPN extension then this is the protocol selected, ++ %% otherwise handle the NPN extension. ++ ALPN = maps:get(alpn, Exts, undefined), ++ case decode_alpn(ALPN) of ++ %% ServerHello contains exactly one protocol: the one selected. ++ %% We also ignore the ALPN extension during renegotiation (see encode_alpn/2). ++ [Protocol] when not Renegotiation -> ++ validate_application_protocol(Protocol, ++ maps:get(alpn_advertised_protocols, SslOpts)), ++ {ConnectionStates, alpn, Protocol, StaplingState}; ++ [_] when Renegotiation -> ++ {ConnectionStates, alpn, undefined, StaplingState}; ++ undefined -> ++ NextProtocolNegotiation = maps:get(next_protocol_negotiation, Exts, undefined), ++ NextProtocolSelector = maps:get(next_protocol_selector, SslOpts, undefined), ++ Protocol = handle_next_protocol(NextProtocolNegotiation, NextProtocolSelector, Renegotiation), ++ {ConnectionStates, npn, Protocol, StaplingState}; ++ {error, Reason} -> ++ throw(?ALERT_REC(?FATAL, ?HANDSHAKE_FAILURE, Reason)); ++ [] -> ++ throw(?ALERT_REC(?FATAL, ?HANDSHAKE_FAILURE, no_protocols_in_server_hello)); ++ [_|_] -> ++ throw(?ALERT_REC(?FATAL, ?HANDSHAKE_FAILURE, too_many_protocols_in_server_hello)) + end. + + select_curve(Client, Server) -> +@@ -3501,19 +3486,53 @@ filter_unavailable_ecc_suites(no_curve, Suites) -> + filter_unavailable_ecc_suites(_, Suites) -> + Suites. + %%-------------Extension handling -------------------------------- ++validate_cipher_suite(CipherSuite, ClientCipherSuites) -> ++ case lists:member(CipherSuite, ClientCipherSuites) of ++ true -> ok; ++ false -> throw(?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER)) ++ end. ++ + +-handle_renegotiation_extension(Role, RecordCB, Version, Info, Random, NegotiatedCipherSuite, ++handle_renegotiation_extension(Role, RecordCB, Version, Info, Random, ++ NegotiatedCipherSuite, + ClientCipherSuites, +- ConnectionStates0, Renegotiation, SecureRenegotation) -> +- {ok, ConnectionStates} = handle_renegotiation_info(Version, RecordCB, Role, Info, ConnectionStates0, +- Renegotiation, SecureRenegotation, +- ClientCipherSuites), ++ ConnectionStates0, ++ Renegotiation, SecureRenegotation) -> ++ {ok, ConnectionStates} = ++ handle_renegotiation_info(Version, RecordCB, Role, Info, ++ ConnectionStates0, ++ Renegotiation, SecureRenegotation, ++ ClientCipherSuites), + hello_pending_connection_states(RecordCB, Role, + Version, + NegotiatedCipherSuite, + Random, + ConnectionStates). + ++assert_max_frag_length(true, Exts, ConnectionStates) -> ++ %% RFC 6066: handle received/expected maximum fragment length ++ ServerMaxFragEnum = maps:get(max_frag_enum, Exts, undefined), ++ ConnMaxFragLen = maps:get(max_fragment_length, ConnectionStates, undefined), ++ ClientMaxFragEnum = max_frag_enum(ConnMaxFragLen), ++ if ServerMaxFragEnum == ClientMaxFragEnum -> ++ ok; ++ true -> ++ throw(?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER)) ++ end; ++assert_max_frag_length(_, _, _) -> ++ ok. ++ ++validate_application_protocol(_, undefined) -> ++ %% Server sent ALPN protocol not requested by client ++ throw(?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, unexpected_alpn)); ++validate_application_protocol(Alpn, ClientAlpn) -> ++ case lists:member(Alpn, ClientAlpn) of ++ true -> ++ ok; ++ false -> ++ throw(?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, not_advertised_alpn)) ++ end. ++ + %% Receive protocols, choose one from the list, return it. + handle_alpn_extension(_, {error, Reason}) -> + throw(?ALERT_REC(?FATAL, ?HANDSHAKE_FAILURE, Reason)); +diff --git a/lib/ssl/test/tls_api_SUITE.erl b/lib/ssl/test/tls_api_SUITE.erl +index e1cfbfaff0..bb1b7949b5 100644 +--- a/lib/ssl/test/tls_api_SUITE.erl ++++ b/lib/ssl/test/tls_api_SUITE.erl +@@ -114,7 +114,11 @@ + reuseaddr/0, + reuseaddr/1, + signature_algs/0, +- signature_algs/1 ++ signature_algs/1, ++ tls_reject_unoffered_cipher_suite/0, ++ tls_reject_unoffered_cipher_suite/1, ++ tls_reject_unoffered_alpn/0, ++ tls_reject_unoffered_alpn/1 + ]). + + %% Apply export +@@ -130,6 +134,13 @@ + + -define(SLEEP, 500). + -define(CORRECT_PASSWORD, "hello test"). ++ ++%% Rogue-server tests: an anonymous suite a default client never offers, and an ++%% ordinary suite pinned so the ALPN test can echo an *offered* suite. ++-define(ROGUE_ANON_SUITE, ?TLS_DH_anon_WITH_AES_128_CBC_SHA). ++-define(ALPN_TEST_SUITE_BIN, <<16#C0, 16#2F>>). %% TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ++-define(ALPN_TEST_SUITE_MAP, #{key_exchange => ecdhe_rsa, cipher => aes_128_gcm, ++ mac => aead, prf => sha256}). + -define(INCORRECT_PASSWORD, "hello"). + -define(BADARG_PASSWORD, hello). + +@@ -142,7 +153,8 @@ all() -> + {group, 'tlsv1.3'}, + {group, 'tlsv1.2'}, + {group, 'tlsv1.1'}, +- {group, 'tlsv1'} ++ {group, 'tlsv1'}, ++ {group, rogue_server_tests} + ]. + + groups() -> +@@ -151,7 +163,8 @@ groups() -> + tls_13_middlebox_reject_change_cipher_spec_as_first_msg]) -- [sockname]}, + {'tlsv1.2', [], api_tests()}, + {'tlsv1.1', [], api_tests()}, +- {'tlsv1', [], api_tests()} ++ {'tlsv1', [], api_tests()}, ++ {rogue_server_tests, [parallel], rogue_server_tests()} + ]. + + api_tests() -> +@@ -191,6 +204,10 @@ api_tests() -> + reuseaddr + ]. + ++rogue_server_tests() -> ++ [tls_reject_unoffered_cipher_suite, ++ tls_reject_unoffered_alpn]. ++ + init_per_suite(Config0) -> + catch crypto:stop(), + try crypto:start() of +@@ -769,6 +786,153 @@ tls_dont_crash_on_handshake_garbage(Config) -> + ssl_test_lib:check_server_alert(Server, handshake_failure) + end. + ++%%-------------------------------------------------------------------- ++tls_reject_unoffered_cipher_suite() -> ++ [{doc, "An on-path attacker forges a TLS-1.2 ServerHello selecting an " ++ "anonymous cipher suite the client never offered. A correct client must " ++ "reject it with a fatal illegal_parameter alert instead of entering the " ++ "anonymous (certificate-less) key exchange, which would bypass " ++ "verify_peer. Regression test for the pre TLS-1.3 client cipher suite " ++ "check."}]. ++tls_reject_unoffered_cipher_suite(Config) when is_list(Config) -> ++ %% verify_peer with proper cacerts, so the only thing standing between the ++ %% client and a server-authentication bypass is the cipher suite check. ++ ClientOpts = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config), ++ rogue_server_illegal_parameter(cipher_suite, ClientOpts). ++ ++%%-------------------------------------------------------------------- ++tls_reject_unoffered_alpn() -> ++ [{doc, "An on-path attacker forges a TLS-1.2 ServerHello whose ALPN " ++ "extension selects a protocol the client never advertised. A correct " ++ "client must reject it with a fatal illegal_parameter alert. Regression " ++ "test for the pre TLS-1.3 client ALPN check."}]. ++tls_reject_unoffered_alpn(Config) when is_list(Config) -> ++ ClientOpts0 = ssl_test_lib:ssl_options(client_rsa_opts, Config), ++ %% Advertise a single protocol; the attacker will answer with a different ++ %% one. Pin the offered cipher suite so the forged ServerHello can echo an ++ %% *offered* suite and thereby isolate the ALPN check as the sole reason for ++ %% rejection. ++ ClientOpts = [{alpn_advertised_protocols, [<<"offered/1">>]}, ++ {ciphers, [?ALPN_TEST_SUITE_MAP]} | ClientOpts0], ++ rogue_server_illegal_parameter(alpn, ClientOpts). ++ ++%% Drive a real ssl client against a raw-TCP attacker that hand-forges a ++%% TLS-1.2 server flight, and assert the client aborts with illegal_parameter. ++rogue_server_illegal_parameter(Kind, ClientOpts) -> ++ {ok, LSock} = gen_tcp:listen(0, [binary, {active, false}, ++ {reuseaddr, true}, {packet, 0}]), ++ {ok, Port} = inet:port(LSock), ++ Parent = self(), ++ Attacker = spawn_link(fun() -> Parent ! {attacker, run_rogue_server(LSock, Kind)} end), ++ Opts = [{versions, ['tlsv1.2']}, {active, false}, ++ {server_name_indication, disable} | ClientOpts], ++ Result = ssl:connect("localhost", Port, Opts, 5000), ++ gen_tcp:close(LSock), ++ Observed = receive {attacker, O} -> O after 5000 -> unlink(Attacker), timeout end, ++ ct:log("ssl:connect returned: ~p~nattacker observed: ~p", [Result, Observed]), ++ %% Primary assertion: the client aborts the handshake with the mandated alert. ++ case Result of ++ {error, {tls_alert, {illegal_parameter, _}}} -> ++ ok; ++ Other -> ++ ct:fail("Expected illegal_parameter alert, client returned ~p " ++ "(attacker observed ~p)", [Other, Observed]) ++ end, ++ %% Secondary assertion: the client must not have proceeded past ServerHello ++ %% into the handshake (e.g. ClientKeyExchange, msg type 16). ++ case Observed of ++ {client_handshake, MsgType} -> ++ ct:fail("Client accepted the forged ServerHello and continued the " ++ "handshake (msg type ~p) instead of alerting", [MsgType]); ++ _ -> ++ ok ++ end. ++ ++%% ---- Raw-TCP attacker: forges the TLS-1.2 server flight -------------------- ++run_rogue_server(LSock, Kind) -> ++ {ok, Sock} = gen_tcp:accept(LSock, 5000), ++ case rogue_read_record(Sock) of ++ {?HANDSHAKE, _ClientHello} -> ++ ok = gen_tcp:send(Sock, rogue_server_flight(Kind)), ++ Obs = rogue_observe(Sock), ++ gen_tcp:close(Sock), ++ Obs; ++ Other -> ++ gen_tcp:close(Sock), ++ {error, {unexpected_first_record, Other}} ++ end. ++ ++rogue_server_flight(cipher_suite) -> ++ %% ServerHello selecting the never-offered anonymous suite, no Certificate, ++ %% an unsigned anon-DH ServerKeyExchange and ServerHelloDone. On code that ++ %% lacks the cipher suite check the client would reply with ClientKeyExchange. ++ SH = rogue_server_hello(?ROGUE_ANON_SUITE, <<>>), ++ SKE = rogue_anon_server_key_exchange(), ++ SHD = rogue_handshake(?SERVER_HELLO_DONE, <<>>), ++ rogue_record(?HANDSHAKE, <>); ++rogue_server_flight(alpn) -> ++ %% ServerHello echoing an *offered* cipher suite but whose ALPN extension ++ %% selects a protocol the client never advertised. ++ AlpnExt = rogue_alpn_extension(<<"unoffered/1">>), ++ SH = rogue_server_hello(?ALPN_TEST_SUITE_BIN, AlpnExt), ++ rogue_record(?HANDSHAKE, SH). ++ ++rogue_observe(Sock) -> ++ case rogue_read_record(Sock) of ++ {?HANDSHAKE, <>} -> ++ {client_handshake, MsgType}; ++ {?ALERT, <>} -> ++ {client_alert, Level, Desc}; ++ {error, closed} -> ++ client_closed; ++ Other -> ++ {other, Other} ++ end. ++ ++rogue_server_hello(CipherSuite, Extensions) -> ++ Random = crypto:strong_rand_bytes(32), ++ SessionId = <<>>, ++ ExtLen = byte_size(Extensions), ++ Body = <<3, 3, %% legacy_version = TLS 1.2 ++ Random/binary, ++ (byte_size(SessionId)), SessionId/binary, ++ CipherSuite/binary, ++ 0, %% compression = null ++ ExtLen:16, Extensions/binary>>, ++ rogue_handshake(?SERVER_HELLO, Body). ++ ++rogue_alpn_extension(Protocol) -> ++ ProtoList = <<(byte_size(Protocol)), Protocol/binary>>, ++ ExtData = <<(byte_size(ProtoList)):16, ProtoList/binary>>, ++ <>. ++ ++rogue_anon_server_key_exchange() -> ++ %% RFC 3526 MODP group 14 (2048-bit), generator 2, unsigned (anon). ++ P = <<16#FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF:2048>>, ++ G = <<2>>, ++ XPriv = crypto:strong_rand_bytes(32), ++ Ys = crypto:mod_pow(2, binary:decode_unsigned(XPriv), binary:decode_unsigned(P)), ++ Body = <<(byte_size(P)):16, P/binary, ++ (byte_size(G)):16, G/binary, ++ (byte_size(Ys)):16, Ys/binary>>, ++ rogue_handshake(?SERVER_KEY_EXCHANGE, Body). ++ ++rogue_handshake(Type, Body) -> ++ <>. ++ ++rogue_record(ContentType, Payload) -> ++ <>. ++ ++rogue_read_record(Sock) -> ++ case gen_tcp:recv(Sock, 5, 5000) of ++ {ok, <>} -> ++ case gen_tcp:recv(Sock, Len, 5000) of ++ {ok, Payload} -> {CT, Payload}; ++ Err -> Err ++ end; ++ {error, _} = E -> E ++ end. ++ + %%-------------------------------------------------------------------- + tls_tcp_error_propagation_in_active_mode() -> + [{doc,"Test that process receives {ssl_error, Socket, closed} when tcp error ocurres"}]. +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-58227.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-58227.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-58227.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-58227.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,378 @@ +From 7fb568c5883dde68a4a1eca9e51f639a812097b7 Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Fri, 24 Jul 2026 17:04:10 +0200 +Subject: [PATCH] Merge branch 'ingela/maint-27/ssl/cert-chain/OTP-20245' into + maint-27 + +* ingela/maint-27/ssl/cert-chain/OTP-20245: + ssl: Use digraph to ensure robust cert chain building. + +(cherry picked from commit e365625d8c2bc1ce05d393b98185f22243cbd3ad) +--- + lib/ssl/src/ssl_certificate.erl | 145 +++++++++++++++----------------- + lib/ssl/test/ssl_cert_SUITE.erl | 125 ++++++++++++++++++++++++++- + 2 files changed, 193 insertions(+), 77 deletions(-) + +diff --git a/lib/ssl/src/ssl_certificate.erl b/lib/ssl/src/ssl_certificate.erl +index e2a1286c9c..abb62e6d6d 100644 +--- a/lib/ssl/src/ssl_certificate.erl ++++ b/lib/ssl/src/ssl_certificate.erl +@@ -88,6 +88,8 @@ + %% Tracing + -export([handle_trace/3]). + ++-define(MAX_CHAIN, 12). %% In depth a little longer than default MAX_DEPTH ++ + %%==================================================================== + %% Internal application API + %%==================================================================== +@@ -398,8 +400,11 @@ chain_result(Root0, Chain0, both) -> + {DRoot, DChain} = decoded_chain(Root0, Chain0), + {ok, {ERoot, EChain}, {DRoot, DChain}}. + +-build_certificate_chain(#cert{otp=OtpCert}=Cert, CertDbHandle, CertsDbRef, Chain, ListDb) -> +- IssuerAndSelfSigned = ++ ++build_certificate_chain(_,_,_,Chain,_) when length(Chain) >= ?MAX_CHAIN-> ++ {ok, undefined, lists:reverse(Chain)}; ++build_certificate_chain(#cert{otp = OtpCert} = Cert, CertDbHandle, CertsDbRef, Chain, ListDb) -> ++ IssuerAndSelfSigned = + case public_key:pkix_is_self_signed(OtpCert) of + true -> + {public_key:pkix_issuer_id(OtpCert, self), true}; +@@ -421,7 +426,7 @@ build_certificate_chain(#cert{otp=OtpCert}=Cert, CertDbHandle, CertsDbRef, Chain + %% incorrect. + {ok, undefined, lists:reverse(Chain)} + end; +- {{ok, {SerialNr, Issuer}}, SelfSigned} -> ++ {{ok, {SerialNr, Issuer}}, SelfSigned} -> + do_certificate_chain(CertDbHandle, CertsDbRef, Chain, SerialNr, Issuer, SelfSigned, ListDb) + end. + +@@ -431,8 +436,13 @@ do_certificate_chain(_, _, [RootCert | _] = Chain, _, _, true, _) -> + do_certificate_chain(CertDbHandle, CertsDbRef, Chain, SerialNr, Issuer, _, ListDb) -> + case ssl_manager:lookup_trusted_cert(CertDbHandle, CertsDbRef, + SerialNr, Issuer) of +- {ok, Cert} -> +- build_certificate_chain(Cert, CertDbHandle, CertsDbRef, [Cert | Chain], ListDb); ++ {ok, #cert{der = Der} = Cert} -> ++ case lists:any(fun(#cert{der = D}) -> D =:= Der end, Chain) of ++ true -> ++ {ok, undefined, lists:reverse(Chain)}; ++ false -> ++ build_certificate_chain(Cert, CertDbHandle, CertsDbRef, [Cert | Chain], ListDb) ++ end; + _ -> + %% The trusted cert may be obmitted from the chain as the + %% counter part needs to have it anyway to be able to +@@ -481,7 +491,7 @@ find_issuer(#cert{der=DerCert, otp=OtpCert}, CertDbHandle, CertsDbRef, ListDb, I + Result = case is_reference(CertsDbRef) of + true when ListDb == [] -> + CertEntryList = ssl_pkix_db:select_certentries_by_ref(CertsDbRef, CertDbHandle), +- do_find_issuer(IsIssuerFun, CertDbHandle, CertEntryList); ++ do_find_issuer(IsIssuerFun, CertDbHandle, CertEntryList); + false when ListDb == [] -> + {extracted, CertsData} = CertsDbRef, + CertEntryList = [Entry || {decoded, Entry} <- CertsData], +@@ -498,7 +508,7 @@ find_issuer(#cert{der=DerCert, otp=OtpCert}, CertDbHandle, CertsDbRef, ListDb, I + + + do_find_issuer(IssuerFun, CertDbHandle, CertDb) -> +- try ++ try + foldl_db(IssuerFun, CertDbHandle, CertDb) + catch + throw:{ok, _} = Return -> +@@ -694,27 +704,63 @@ paths([#cert{otp=C1}=Cert1, #cert{otp=C2}=Cert2 | Rest], Chain, CertDbHandle, Pa + %% Chain ordered so far + paths([Cert2 | Rest], Chain, CertDbHandle, [Cert1 | Path]); + false -> +- %% Chain is unorded and/or contains extraneous certificates +- unorded_or_extraneous(Chain, CertDbHandle) ++ %% Chain is unordered and/or contains extraneous certificates ++ unorded_or_extraneous(Chain) + end. + +-unorded_or_extraneous([Peer | UnorderedChain], CertDbHandle) -> +- ChainCandidates = extraneous_chains(UnorderedChain), +- lists:map(fun(Candidate) -> +- path_candidate(Peer, Candidate, CertDbHandle) ++unorded_or_extraneous([Peer | ChainCerts]) -> ++ G = digraph:new([acyclic]), ++ try ++ Certs = [Peer | ChainCerts], ++ lists:foreach(fun(Cert) -> ++ digraph:add_vertex(G, cert_id(Cert), Cert) ++ end, Certs), ++ ++ Add = fun(#cert{otp = C1, der = C1Der} = Cert1, #cert{otp = C2} = Cert2) -> ++ case Cert1 =/= Cert2 andalso public_key:pkix_is_issuer(C1, C2) of ++ true -> ++ %% Claim: C2 issued C1 so verify C1's signature with C2's key ++ Signer = C2#'OTPCertificate'.tbsCertificate, ++ case verify_cert_signer(C1Der, Signer) of ++ true -> ++ digraph:add_edge(G, cert_id(Cert1), cert_id(Cert2)); ++ false -> ++ false ++ end; ++ _ -> ++ false ++ end + end, +- ChainCandidates). +- +-path_candidate(Cert, ChainCandidateCAs, CertDbHandle) -> +- {ok, ExtractedCerts} = ssl_pkix_db:extract_trusted_certs({der_otp, ChainCandidateCAs}), +- %% certificate_chain/4 will make sure the chain is ordered +- case build_certificate_chain(Cert, CertDbHandle, ExtractedCerts, [Cert], []) of +- {ok, undefined, Chain} -> +- lists:reverse(Chain); +- {ok, Root, Chain} -> +- [Root | lists:reverse(Chain)] ++ ++ _ = [Add(C1, C2) || C1 <- Certs, C2 <- Certs], ++ ++ %% Path endpoints: certs with no issuer in the sent chain ++ %% (either self-signed or issuer in trust store — handle_partial_chain ++ %% resolves which case applies downstream) ++ Endpoints = [V || V <- digraph:vertices(G), ++ digraph:out_degree(G, V) =:= 0], ++ PeerId = cert_id(Peer), ++ Paths = lists:filtermap( ++ fun(RootId) -> ++ case digraph:get_path(G, PeerId, RootId) of ++ false -> ++ false; ++ VPath -> ++ RevPath = [element(2, digraph:vertex(G, V)) || V <- VPath], ++ {true, lists:reverse(RevPath)} ++ end ++ end, Endpoints), ++ ++ %% Return candidate paths ++ Paths ++ after ++ digraph:delete(G) + end. + ++cert_id(#cert{der = Der}) -> ++ %% Use a hash as vertex ID for efficient comparison ++ crypto:hash(sha256, Der). ++ + handle_partial_chain([#cert{der=DERIssuerCert, otp=OtpIssuerCert}=Cert| Rest] = Path, PartialChainHandler, + CertDbHandle, CertDbRef) -> + case public_key:pkix_is_self_signed(OtpIssuerCert) of +@@ -790,59 +836,6 @@ handle_incomplete_chain([#cert{}=Peer| _] = Chain0, PartialChainHandler, Default + Default + end. + +-extraneous_chains(Certs) -> +- %% If some certs claim to be the same cert that is have the same +- %% subject field we should create a list of possible chain certs +- %% for each such cert. Only one chain, if any, should be +- %% verifiable using available ROOT certs. +- Subjects = [{subject(OTP), Cert} || #cert{otp=OTP} = Cert <- Certs], +- Duplicates = find_duplicates(Subjects), +- %% Number of certs with duplicates (same subject) has been limited +- %% to 4 and the maximum number of combinations is limited to 16. +- build_candidates(Duplicates, 4, 16). +- +-build_candidates(Map, Duplicates, Combinations) -> +- Subjects = maps:keys(Map), +- build_candidates(Subjects, Map, Duplicates, 1, Combinations, []). +-%% +-build_candidates([], _, _, _, _, Acc) -> +- Acc; +-build_candidates([H|T], Map, Duplicates, Combinations, Max, Acc0) -> +- case maps:get(H, Map) of +- {Certs, Counter} when Counter > 1 andalso +- Duplicates > 0 andalso +- Counter * Combinations =< Max -> +- case Acc0 of +- [] -> +- Acc = [[Cert] || Cert <- Certs], +- build_candidates(T, Map, Duplicates - 1, Combinations * Counter, Max, Acc); +- _Else -> +- Acc = [[Cert|L] || Cert <- Certs, L <- Acc0], +- build_candidates(T, Map, Duplicates - 1, Combinations * Counter, Max, Acc) +- end; +- {[Cert|_Throw], _Counter} -> +- case Acc0 of +- [] -> +- Acc = [[Cert]], +- build_candidates(T, Map, Duplicates, Combinations, Max, Acc); +- _Else -> +- Acc = [[Cert|L] || L <- Acc0], +- build_candidates(T, Map, Duplicates, Combinations, Max, Acc) +- end +- end. +- +-find_duplicates(Chain) -> +- find_duplicates(Chain, #{}). +-%% +-find_duplicates([], Acc) -> +- Acc; +-find_duplicates([{Subject, Cert}|T], Acc) -> +- case maps:get(Subject, Acc, none) of +- none -> +- find_duplicates(T, Acc#{Subject => {[Cert], 1}}); +- {Certs, Counter} -> +- find_duplicates(T, Acc#{Subject => {[Cert|Certs], Counter + 1}}) +- end. + + subject(Cert) -> + {_Serial,Subject} = public_key:pkix_subject_id(Cert), +diff --git a/lib/ssl/test/ssl_cert_SUITE.erl b/lib/ssl/test/ssl_cert_SUITE.erl +index 0aacf379c1..3e45c52eda 100644 +--- a/lib/ssl/test/ssl_cert_SUITE.erl ++++ b/lib/ssl/test/ssl_cert_SUITE.erl +@@ -119,6 +119,12 @@ + cross_signed_chain/1, + expired_root_with_cross_signed_root/0, + expired_root_with_cross_signed_root/1, ++ malicious_cycle_in_peer_chain/0, ++ malicious_cycle_in_peer_chain/1, ++ max_chain_depth_buildup/0, ++ max_chain_depth_buildup/1, ++ duplicate_issuer_in_trust_store/0, ++ duplicate_issuer_in_trust_store/1, + key_auth_ext_sign_only/0, + key_auth_ext_sign_only/1, + hello_retry_request/0, +@@ -227,7 +233,10 @@ rsa_tests() -> + [ + longer_chain, + cross_signed_chain, +- expired_root_with_cross_signed_root ++ expired_root_with_cross_signed_root, ++ malicious_cycle_in_peer_chain, ++ max_chain_depth_buildup, ++ duplicate_issuer_in_trust_store + ]. + + tls_1_3_rsa_tests() -> +@@ -1281,6 +1290,120 @@ expired_root_with_cross_signed_root(Config) when is_list(Config) -> + {cacerts, [AltCrossRoot | ClientCas0]} | proplists:delete(cacerts, ClientOpts)], + ServerOpts, Config). + ++%%-------------------------------------------------------------------- ++malicious_cycle_in_peer_chain() -> ++ [{doc, "A malicious client sends an unordered chain to a server. " ++ "The server processes it through unorded_or_extraneous/2 which " ++ "uses an acyclic digraph. Verify that path construction from " ++ "the unordered chain terminates and does not loop."}]. ++malicious_cycle_in_peer_chain(Config) when is_list(Config) -> ++ Key1 = ssl_test_lib:hardcode_rsa_key(1), ++ Key2 = ssl_test_lib:hardcode_rsa_key(2), ++ Key3 = ssl_test_lib:hardcode_rsa_key(3), ++ Key4 = ssl_test_lib:hardcode_rsa_key(4), ++ Key5 = ssl_test_lib:hardcode_rsa_key(5), ++ ++ %% Client chain with cross-key intermediates ++ #{client_config := ClientOpts0} = ++ public_key:pkix_test_data( ++ #{server_chain => #{root => [{key, Key4}], ++ peer => [{key, Key5}]}, ++ client_chain => #{root => [{key, Key1}], ++ intermediates => [[{key, Key2}], [{key, Key1}]], ++ peer => [{key, Key3}]}}), ++ ++ %% Build the client's ordered chain ++ ClientCert = proplists:get_value(cert, ClientOpts0), ++ ClientCAs = proplists:get_value(cacerts, ClientOpts0), ++ {ok, ExtractedCAs} = ssl_pkix_db:extract_trusted_certs({der, ClientCAs}), ++ {ok, _, [Peer, CA1, CA2, Root]} = ++ ssl_certificate:certificate_chain(ClientCert, ets:new(foo, []), ++ ExtractedCAs, [], encoded), ++ ++ %% Shuffle chain so it's unordered — triggers unorded_or_extraneous ++ MaliciousChain = [Peer, Root, CA2, CA1], ++ CertRecs = [#cert{der=D, otp=public_key:pkix_decode_cert(D, otp)} ++ || D <- MaliciousChain], ++ ++ %% Call trusted_cert_and_paths directly — this is the code path ++ %% that would hang without the digraph fix ++ %% Use empty trust store so no path can be validated ++ Result = ssl_certificate:trusted_cert_and_paths( ++ CertRecs, ets:new(foo, []), {extracted, []}, ++ fun(_) -> unknown_ca end), ++ ++ %% Must return (not hang) with unknown_ca for all paths ++ lists:foreach(fun({unknown_ca, _}) -> ok; ++ ({#cert{}, _}) -> ok ++ end, Result). ++ ++%%-------------------------------------------------------------------- ++max_chain_depth_buildup() -> ++ [{doc, "Chain building stops at MAX_CHAIN (12) even when the trust " ++ "store contains a longer valid chain. Guards against resource " ++ "exhaustion from very deep chains."}]. ++max_chain_depth_buildup(Config) when is_list(Config) -> ++ %% Create chain with 15 intermediates — exceeds MAX_CHAIN (12) ++ Keys = [ssl_test_lib:hardcode_rsa_key((N rem 6) + 1) ++ || N <- lists:seq(1, 17)], ++ [RootKey, PeerKey | CAKeys] = Keys, ++ IntermediateOpts = [[{key, K}] || K <- CAKeys], ++ ++ #{server_config := ServerOpts} = ++ public_key:pkix_test_data( ++ #{server_chain => #{root => [{key, RootKey}], ++ intermediates => IntermediateOpts, ++ peer => [{key, PeerKey}]}, ++ client_chain => #{root => [{key, RootKey}], ++ peer => [{key, PeerKey}]}}), ++ ++ SCert = proplists:get_value(cert, ServerOpts), ++ SCerts = proplists:get_value(cacerts, ServerOpts), ++ {ok, ExtractedCAs} = ssl_pkix_db:extract_trusted_certs({der, SCerts}), ++ ++ %% Build chain — must terminate and respect the MAX_CHAIN limit ++ {ok, _Root, Chain} = ++ ssl_certificate:certificate_chain(SCert, ets:new(foo, []), ++ ExtractedCAs, [], encoded), ++ %% MAX_CHAIN is 12: chain must not exceed that ++ true = (length(Chain) =< 12). ++ ++%%-------------------------------------------------------------------- ++duplicate_issuer_in_trust_store() -> ++ [{doc, "Trust store lookup returns a cert already in the chain. " ++ "The duplicate check in do_certificate_chain must detect this " ++ "and terminate instead of looping. Tests the DER-based " ++ "duplicate guard added in OTP-20245."}]. ++duplicate_issuer_in_trust_store(Config) when is_list(Config) -> ++ Key1 = ssl_test_lib:hardcode_rsa_key(1), ++ Key2 = ssl_test_lib:hardcode_rsa_key(2), ++ Key3 = ssl_test_lib:hardcode_rsa_key(3), ++ ++ #{server_config := ServerOpts0} = ++ public_key:pkix_test_data( ++ #{server_chain => #{root => [{key, Key1}], ++ intermediates => [[{key, Key2}]], ++ peer => [{key, Key3}]}, ++ client_chain => #{root => [{key, Key1}], ++ peer => [{key, Key3}]}}), ++ ++ SCert = proplists:get_value(cert, ServerOpts0), ++ SCerts = proplists:get_value(cacerts, ServerOpts0), ++ ++ %% Add peer cert to trust store — creates potential for ++ %% lookup_trusted_cert to return a cert already in chain ++ PoisonedCAs = [SCert | SCerts], ++ {ok, ExtractedCAs} = ssl_pkix_db:extract_trusted_certs({der, PoisonedCAs}), ++ ++ %% Must terminate (not hang) and produce a valid chain ++ {ok, _Root, Chain} = ++ ssl_certificate:certificate_chain(SCert, ets:new(foo, []), ++ ExtractedCAs, [], encoded), ++ %% No duplicates in result ++ true = (length(Chain) =:= length(lists:usort(Chain))), ++ %% Reasonable length (normal: peer + CA + root = 3) ++ true = (length(Chain) =< 4). ++ + %%-------------------------------------------------------------------- + %% TLS 1.3 Test cases ----------------------------------------------- + %%-------------------------------------------------------------------- +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-59250.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-59250.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-59250.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-59250.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,52 @@ +From b2bdbcc69a00144b3d3b4f0b99d9fab4030d24bd Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Fri, 24 Jul 2026 17:04:11 +0200 +Subject: [PATCH] Merge branch 'kuba/megaco/sprintf-overflow/OTP-20237' into + maint-27 + +* kuba/megaco/sprintf-overflow/OTP-20237: + megaco: fix sprintf buffer overflow in flex scanner + +(cherry picked from commit ea39889874a2a3c65ce91e3a3d90b318d44a2765) +--- + lib/megaco/src/flex/megaco_flex_scanner_drv.flex.src | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +diff --git a/lib/megaco/src/flex/megaco_flex_scanner_drv.flex.src b/lib/megaco/src/flex/megaco_flex_scanner_drv.flex.src +index 50cf5f7d5f..f0895ce6a4 100644 +--- a/lib/megaco/src/flex/megaco_flex_scanner_drv.flex.src ++++ b/lib/megaco/src/flex/megaco_flex_scanner_drv.flex.src +@@ -821,7 +821,7 @@ static void mfs_alloc_failed(MfsErlDrvData* dataP, char* msg, int sz) + + int msg_len = strlen(msg); + if ((10 + 10 + msg_len) < sizeof(dataP->error_msg)) { +- if (0 >= sprintf(dataP->error_msg, "%s of %d bytes", msg, sz)) { ++ if (0 >= snprintf(dataP->error_msg, sizeof(dataP->error_msg), "%s of %d bytes", msg, sz)) { + mfs_fatal_error(dataP, msg); + } + } else { +@@ -1156,8 +1156,8 @@ static void mfs_load_property_groups(MfsErlDrvData* dataP) + * }). + */ + +- if (0 >= sprintf(dataP->error_msg, "%s %s %s", +- PG_ERR_PRE, PG_ERR1, name)) { ++ if (0 >= snprintf(dataP->error_msg, sizeof(dataP->error_msg), ++ "%s %s %s", PG_ERR_PRE, PG_ERR1, name)) { + mfs_fatal_error(dataP, PG_ERR1); + } + dataP->error = TRUE; +@@ -1237,8 +1237,8 @@ static void mfs_load_property_groups(MfsErlDrvData* dataP) + "property parm name not found when " + "nameStart = %d\n", nameStart) ); + +- if (0 >= sprintf(dataP->error_msg, "%s %s (name start at %d)", +- PG_ERR_PRE, PG_ERR2, nameStart)) { ++ if (0 >= snprintf(dataP->error_msg, sizeof(dataP->error_msg), ++ "%s %s (name start at %d)", PG_ERR_PRE, PG_ERR2, nameStart)) { + mfs_fatal_error(dataP, PG_ERR2); + } + +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-59251.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-59251.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-59251.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-59251.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,581 @@ +From dbd306028e9938f1f2b529d800fac09bafd79c10 Mon Sep 17 00:00:00 2001 +From: Erlang/OTP +Date: Fri, 24 Jul 2026 17:04:14 +0200 +Subject: [PATCH] Merge branch + 'kuba/maint-27/public_key/policy_tree_exhaustion_fix/OTP-20251' into maint-27 + +* kuba/maint-27/public_key/policy_tree_exhaustion_fix/OTP-20251: + public_key: Cap policy tree growth to prevent DoS + +(cherry picked from commit d221f88880bdf458c008e716c616c76f8425d984) +--- + lib/public_key/include/public_key.hrl | 2 + + lib/public_key/src/pubkey_cert.erl | 153 ++++++++++-------- + lib/public_key/src/pubkey_policy_tree.erl | 54 +++++-- + lib/public_key/src/public_key.erl | 6 + + .../test/pubkey_policy_tree_SUITE.erl | 47 +++++- + lib/ssl/src/ssl_handshake.erl | 2 + + 6 files changed, 172 insertions(+), 92 deletions(-) + +diff --git a/lib/public_key/include/public_key.hrl b/lib/public_key/include/public_key.hrl +index 6670e0e524..696a5f74a8 100644 +--- a/lib/public_key/include/public_key.hrl ++++ b/lib/public_key/include/public_key.hrl +@@ -44,6 +44,8 @@ + -record(path_validation_state, + { + valid_policy_tree, ++ %% Monotonic count of nodes ever added to policy tree (never decremented by pruning). ++ policy_tree_node_count = 1 :: non_neg_integer(), + user_initial_policy_set, + explicit_policy, + inhibit_any_policy, +diff --git a/lib/public_key/src/pubkey_cert.erl b/lib/public_key/src/pubkey_cert.erl +index 64334383c1..56f4729805 100644 +--- a/lib/public_key/src/pubkey_cert.erl ++++ b/lib/public_key/src/pubkey_cert.erl +@@ -56,7 +56,7 @@ + x509_pkix_sign_types/1, + root_cert/2]). + +--define(NULL, 0). ++-define(MAX_POLICY_TREE_NODES, 1000). + + %%==================================================================== + %% Internal application APIs +@@ -822,13 +822,14 @@ validate_extensions(Cert, [#'Extension'{extnID = ?'id-ce-certificatePolicies', + | Rest], + ValidationState, + ExistBasicCon, SelfSigned, UserState, VerifyFun) -> +- Tree = process_policy_tree(Info, SelfSigned, ValidationState), ++ {Tree, NodeCount} = process_policy_tree(Info, SelfSigned, ValidationState), + validate_extensions(Cert, Rest, + ValidationState#path_validation_state{ + policy_ext_present = true, + current_any_policy_qualifiers = + current_any_policy_qualifiers(Info), +- valid_policy_tree = Tree}, ++ valid_policy_tree = Tree, ++ policy_tree_node_count = NodeCount}, + ExistBasicCon, SelfSigned, UserState, VerifyFun); + validate_extensions(Cert, [#'Extension'{extnID = ?'id-ce-policyConstraints'} = Ext + | Rest], ValidationState, ExistBasicCon, +@@ -875,8 +876,9 @@ validate_extensions(Cert, [#'Extension'{} = Extension | Rest], + UserState, VerifyFun). + + handle_last_cert(Cert, #path_validation_state{last_cert = true, +- user_initial_policy_set = PolicySet, +- valid_policy_tree = Tree} = ValidationState0) -> ++ user_initial_policy_set = PolicySet, ++ valid_policy_tree = Tree, ++ policy_tree_node_count = NodeCount0} = ValidationState0) -> + OtpCert = otp_cert(Cert), + TBSCert = OtpCert#'OTPCertificate'.tbsCertificate, + Extensions = +@@ -891,9 +893,13 @@ handle_last_cert(Cert, #path_validation_state{last_cert = true, + _ -> + ValidationState0 + end, +- ValidTree = policy_tree_intersection(PolicySet, Tree), ++ %% No assert needed — growth here is bounded by UserPolicySet size ++ %% (relying-party config, not attacker-controlled). Terminal operation ++ %% with no subsequent amplification possible. ++ {ValidTree, NodeCount} = policy_tree_intersection(PolicySet, Tree, NodeCount0), + validate_policy_tree(Cert, +- ValidationState#path_validation_state{valid_policy_tree = ValidTree}); ++ ValidationState#path_validation_state{valid_policy_tree = ValidTree, ++ policy_tree_node_count = NodeCount}); + handle_last_cert(_, ValidationState) -> + ValidationState. + +@@ -959,24 +965,28 @@ assert_valid_policy_tree(false, _Tree) -> % 6.1.3 e + %% certificate and the valid_policy_tree is not NULL, process the + %% policy information by performing the following steps in order: + process_policy_tree(PolicyInformation, SelfSigned, +- #path_validation_state{valid_policy_tree = Tree0} = +- ValidationState) -> ++ #path_validation_state{valid_policy_tree = Tree0, ++ policy_tree_node_count = NodeCount0} = ++ ValidationState) -> + case pubkey_policy_tree:is_empty(Tree0) of + true -> +- Tree0; ++ {Tree0, NodeCount0}; + false -> + %% Step 1 & 2 +- Tree = add_policy_children(PolicyInformation, ++ {Tree, NodeCount} = add_policy_children(PolicyInformation, + SelfSigned, ValidationState), ++ assert_policy_tree_node_count(NodeCount), + %% Step 3: If there is a node in the valid_policy_tree of depth i-1 or + %% less without any child nodes, delete that node. Repeat this step + %% until there are no nodes of depth i-1 or less without children. +- pubkey_policy_tree:prune_tree(Tree) ++ PrunedTree = pubkey_policy_tree:prune_tree(Tree), ++ {PrunedTree, NodeCount} + end. + + %% 6.1.3 d + add_policy_children(PolicyInfoList0, SelfSigned, + #path_validation_state{valid_policy_tree = Tree0, ++ policy_tree_node_count = NodeCount0, + inhibit_any_policy = AnyPolicyConstraint, + cert_num = CertNum, + max_path_length = PathLen +@@ -995,17 +1005,16 @@ add_policy_children(PolicyInfoList0, SelfSigned, + fun(#{expected_policy_set := ExpPolicySet}) -> + policy_children(ExpPolicySet, PolicyInfoList) + end, +- Tree1 = pubkey_policy_tree:add_leaves(Tree0, LeafFun), +- ++ {Tree1, NodeCount1} = pubkey_policy_tree:add_leaves(Tree0, NodeCount0, LeafFun), + %% posibly ii + AllLeaves = pubkey_policy_tree:all_leaves(Tree1), + Siblings = fun(#{valid_policy := ?anyPolicy}) -> + any_policy_children(AllLeaves, PolicyInfoList); + (_) -> [] + end, +- Tree = pubkey_policy_tree:add_leaf_siblings(Tree1, Siblings), ++ {Tree, NodeCount} = pubkey_policy_tree:add_leaf_siblings(Tree1, NodeCount1, Siblings), + %% Step 2 +- handle_any_ext(Tree, AnyExt, AnyPolicyConstraint, SelfSigned, CertNum, PathLen). ++ handle_any_ext({Tree, NodeCount}, AnyExt, AnyPolicyConstraint, SelfSigned, CertNum, PathLen). + + %% 6.1.3 - d 1 i + %% Step 1: For each policy P not equal to anyPolicy in the certificate +@@ -1057,9 +1066,9 @@ any_policy_children(_, _) -> + %% expected_policy_set in the parent node, set the qualifier_set to + %% AP-Q, and set the expected_policy_set to the value in the + %% valid_policy from this node. +-handle_any_ext(Tree, undefined, _, _, _,_) -> +- Tree; +-handle_any_ext(Tree, #'PolicyInformation'{ ++handle_any_ext({Tree, NodeCount}, undefined, _, _, _,_) -> ++ {Tree, NodeCount}; ++handle_any_ext({Tree, NodeCount0}, #'PolicyInformation'{ + policyIdentifier = ?anyPolicy, + policyQualifiers = Qualifiers}, AnyPolicyConstraint, + SelfSigned, CertNum, PathLen) -> +@@ -1070,9 +1079,9 @@ handle_any_ext(Tree, #'PolicyInformation'{ + Siblings = fun(Node) -> + any_ext_policy_children(Node, Qualifiers, AllLeaves) + end, +- pubkey_policy_tree:add_leaf_siblings(Tree, Siblings); ++ pubkey_policy_tree:add_leaf_siblings(Tree, NodeCount0, Siblings); + false -> +- Tree ++ {Tree, NodeCount0} + end. + + any_ext_policy_children(#{expected_policy_set := ExpPolicySet}, Qualifiers, AllLeaves) -> +@@ -1087,23 +1096,26 @@ any_ext_policy_children(#{expected_policy_set := ExpPolicySet}, Qualifiers, AllL + %% 6.1.4. b start: + handle_policy_mappings(Cert, + #path_validation_state{valid_policy_tree = Tree0, ++ policy_tree_node_count = NodeCount0, + policy_mapping_ext = + #'Extension'{extnID = ?'id-ce-policyMappings', + extnValue = PolicyMappings}} + = ValidationState) -> +- case handle_policy_mappings(PolicyMappings, Cert, Tree0, ValidationState) of +- {tree, Tree} -> +- ValidationState#path_validation_state{valid_policy_tree = Tree}; ++ case handle_policy_mappings(PolicyMappings, Cert, Tree0, NodeCount0, ValidationState) of ++ {tree, Tree, NodeCount} -> ++ ValidationState#path_validation_state{valid_policy_tree = Tree, ++ policy_tree_node_count = NodeCount}; + {user_state, UState} -> + ValidationState#path_validation_state{user_state = UState} + end. + +-handle_policy_mappings([], _, Tree, _) -> +- {tree, Tree}; +-handle_policy_mappings([Mappings | Rest], Cert, Tree0, ValidationState) -> +- case handle_policy_mapping(Mappings, Cert, Tree0, ValidationState) of +- {tree, Tree} -> +- handle_policy_mappings(Rest, Cert, Tree, ValidationState); ++handle_policy_mappings([], _, Tree, NodeCount, _) -> ++ {tree, Tree, NodeCount}; ++handle_policy_mappings([Mappings | Rest], Cert, Tree0, NodeCount0, ValidationState) -> ++ case handle_policy_mapping(Mappings, Cert, Tree0, NodeCount0, ValidationState) of ++ {tree, Tree, NodeCount} -> ++ assert_policy_tree_node_count(NodeCount), ++ handle_policy_mappings(Rest, Cert, Tree, NodeCount, ValidationState); + Other -> + Other + end. +@@ -1112,24 +1124,20 @@ handle_policy_mappings([Mappings | Rest], Cert, Tree0, ValidationState) -> + %% special value anyPolicy does not appear as an issuerDomainPolicy or + %% a subjectDomainPolicy. + handle_policy_mapping(#'PolicyMappings_SEQOF'{ +- issuerDomainPolicy = +- IssuerPolicy, +- subjectDomainPolicy = +- SubjectPolicy} = Ext, +- Cert, Tree0, +- #path_validation_state{inhibit_policy_mapping = +- PolicyMappingConstraint, +- current_any_policy_qualifiers = +- AnyQualifiers, ++ issuerDomainPolicy = IssuerPolicy, ++ subjectDomainPolicy = SubjectPolicy} = Ext, ++ Cert, Tree0, NodeCount0, ++ #path_validation_state{inhibit_policy_mapping = PolicyMappingConstraint, ++ current_any_policy_qualifiers = AnyQualifiers, + verify_fun = VerifyFun, +- user_state = UserState} +- ) -> ++ user_state = UserState}) -> + case not (?anyPolicy == IssuerPolicy) andalso + not (?anyPolicy == SubjectPolicy) of + true -> +- Tree = handle_policy_mapping_ext(Ext, Tree0, +- PolicyMappingConstraint, AnyQualifiers), +- {tree, Tree}; ++ {Tree, NodeCount} = ++ handle_policy_mapping_ext(Ext, Tree0, NodeCount0, ++ PolicyMappingConstraint, AnyQualifiers), ++ {tree, Tree, NodeCount}; + false -> + UserState = verify_fun(Cert, {bad_cert, {invalid_policy_mapping, Ext}}, + UserState, VerifyFun), +@@ -1138,9 +1146,8 @@ handle_policy_mapping(#'PolicyMappings_SEQOF'{ + + %% 6.1.4. b continue: + handle_policy_mapping_ext(#'PolicyMappings_SEQOF'{ +- issuerDomainPolicy = +- IssuerPolicy}, +- Tree0, 0, _) -> %% 6.1.4. b 2: ++ issuerDomainPolicy = IssuerPolicy}, ++ Tree0, NodeCount, 0, _) -> %% 6.1.4. b 2: + %% (2) If the policy_mapping variable is equal to 0: + + %% (i) delete each node of depth i in the valid_policy_tree where +@@ -1152,11 +1159,11 @@ handle_policy_mapping_ext(#'PolicyMappings_SEQOF'{ + %% children. + + Tree = pubkey_policy_tree:prune_leaves(Tree0, IssuerPolicy), +- pubkey_policy_tree:prune_tree(Tree); ++ {pubkey_policy_tree:prune_tree(Tree), NodeCount}; + handle_policy_mapping_ext(#'PolicyMappings_SEQOF'{ + issuerDomainPolicy = IssuerPolicy, + subjectDomainPolicy = SubjectPolicy}, +- Tree, N, AnyQualifiers) when N > 0 -> %% 6.1.4. b 1: ++ Tree, NodeCount0, N, AnyQualifiers) when N > 0 -> %% 6.1.4. b 1: + + %% (1) If the policy_mapping variable is greater than 0, for each + %% node in the valid_policy_tree of depth i where ID-P is the +@@ -1207,9 +1214,9 @@ handle_policy_mapping_ext(#'PolicyMappings_SEQOF'{ + + case pubkey_policy_tree:map_leaves(Tree, MapPolicy) of + Tree -> %% If no policy was mapped! +- pubkey_policy_tree:add_leaf_siblings(Tree, AnySiblings); ++ pubkey_policy_tree:add_leaf_siblings(Tree, NodeCount0, AnySiblings); + NewTree -> +- NewTree ++ {NewTree, NodeCount0} + end. + + %% 6.1.4 i +@@ -1273,12 +1280,12 @@ maybe_decrement(N, true) -> + + %% Step G from RFC + +-policy_tree_intersection([?anyPolicy], Tree) -> % (ii) from RFC +- Tree; +-policy_tree_intersection(UserPolicySet, Tree0) -> ++policy_tree_intersection([?anyPolicy], Tree, NodeCount) -> % (ii) from RFC ++ {Tree, NodeCount}; ++policy_tree_intersection(UserPolicySet, Tree0, NodeCount0) -> + case pubkey_policy_tree:is_empty(Tree0) of + true -> % (i) from RFC +- Tree0; ++ {Tree0, NodeCount0}; + false -> % (iii) from RFC + %% Step 1 from RFC + ValidPolicyNodeSet = pubkey_policy_tree:valid_policy_node_set(Tree0), +@@ -1288,10 +1295,11 @@ policy_tree_intersection(UserPolicySet, Tree0) -> + Tree1 = pubkey_policy_tree:prune_invalid_nodes(Tree0, InvalidNodes), + + %% Step 3 from RFC +- Tree = handle_any_policy_leaves(Tree1, ValidPolicyNodeSet, UserPolicySet), ++ {Tree, NodeCount} = ++ handle_any_policy_leaves(Tree1, NodeCount0, ValidPolicyNodeSet, UserPolicySet), + + %% Step 4 from RFC +- pubkey_policy_tree:prune_tree(Tree) ++ {pubkey_policy_tree:prune_tree(Tree), NodeCount} + end. + + apply_user_constraints(_, [?anyPolicy]) -> +@@ -1313,21 +1321,22 @@ apply_user_constraints([#{valid_policy := Policy} = Node | Rest], + apply_user_constraints(Rest, UserPolicySet, [Node | Acc]) + end. + +-handle_any_policy_leaves(Tree, _, [?anyPolicy]) -> +- Tree; +-handle_any_policy_leaves(Tree0, ValidPolicyNodeSet, UserPolicySet) -> ++handle_any_policy_leaves(Tree, NodeCount, _, [?anyPolicy]) -> ++ {Tree, NodeCount}; ++handle_any_policy_leaves(Tree0, NodeCount0, ValidPolicyNodeSet, UserPolicySet) -> + case pubkey_policy_tree:any_leaves(Tree0) of + [] -> +- Tree0; ++ {Tree0, NodeCount0}; + AnyLeaves -> +- Tree = add_policy_nodes(AnyLeaves, Tree0, ValidPolicyNodeSet, UserPolicySet), +- pubkey_policy_tree:prune_leaves(Tree, ?anyPolicy) ++ {Tree, NodeCount} = ++ add_policy_nodes(AnyLeaves, Tree0, NodeCount0, ValidPolicyNodeSet, UserPolicySet), ++ {pubkey_policy_tree:prune_leaves(Tree, ?anyPolicy), NodeCount} + end. + +-add_policy_nodes([], Tree, _, _) -> +- Tree; +-add_policy_nodes([#{qualifier_set := Qualifiers} | Rest], Tree0, +- ValidPolicyNodeSet, UserPolicySet) -> ++add_policy_nodes([], Tree, NodeCount, _, _) -> ++ {Tree, NodeCount}; ++add_policy_nodes([#{qualifier_set := Qualifiers} | Rest], ++ Tree0, NodeCount0, ValidPolicyNodeSet, UserPolicySet) -> + PolicySet = [UPolicy || UPolicy <- UserPolicySet, + not pubkey_policy_tree:in_set(UPolicy, ValidPolicyNodeSet)], + Children = +@@ -1336,8 +1345,14 @@ add_policy_nodes([#{qualifier_set := Qualifiers} | Rest], Tree0, + Children; + (_) -> [] + end, +- add_policy_nodes(Rest, pubkey_policy_tree:add_leaf_siblings(Tree0, Siblings), +- ValidPolicyNodeSet, UserPolicySet). ++ {Tree, NodeCount} = pubkey_policy_tree:add_leaf_siblings(Tree0, NodeCount0, Siblings), ++ add_policy_nodes(Rest, Tree, NodeCount, ValidPolicyNodeSet, UserPolicySet). ++ ++%% Monotonic counter — never decremented by pruning. ++assert_policy_tree_node_count(Count) when Count > ?MAX_POLICY_TREE_NODES -> ++ throw({bad_cert, policy_tree_exceeded}); ++assert_policy_tree_node_count(_) -> ++ ok. + + %% End Wrap Up Policy Handling ------------------------------------------------- + +diff --git a/lib/public_key/src/pubkey_policy_tree.erl b/lib/public_key/src/pubkey_policy_tree.erl +index 5830875739..9eb414de3e 100644 +--- a/lib/public_key/src/pubkey_policy_tree.erl ++++ b/lib/public_key/src/pubkey_policy_tree.erl +@@ -23,8 +23,8 @@ + -include("../include/public_key.hrl"). + + %% API +--export([add_leaves/2, +- add_leaf_siblings/2, ++-export([add_leaves/3, ++ add_leaf_siblings/3, + any_leaves/1, + all_leaves/1, + collect_qualifiers/2, +@@ -58,40 +58,49 @@ + %%%=================================================================== + + %%-------------------------------------------------------------------- +--spec add_leaves(policy_tree(), LeafFun) -> policy_tree() when ++-spec add_leaves(policy_tree(), non_neg_integer(), LeafFun) -> ++ {policy_tree(), non_neg_integer()} when + LeafFun :: fun((policy_tree_node()) -> [policy_node()]). + + %% + %% Add leaves specified by calling LeafFun with the current leaves + %% as input + %%-------------------------------------------------------------------- +-add_leaves({Parent, []}, LeafFun) -> +- {Parent, LeafFun(Parent)}; +-add_leaves(Tree, LeafFun0) -> ++add_leaves({Parent, []}, NodeCount0, LeafFun) -> ++ Leaves = LeafFun(Parent), ++ NodeCount = NodeCount0 + length(Leaves), ++ {{Parent, Leaves}, NodeCount}; ++add_leaves(Tree0, NodeCount, LeafFun0) -> + LeafFun = fun(Leaf) -> + NewLeaves = LeafFun0(Leaf), + {Leaf, NewLeaves} + end, +- map_leaves(Tree, LeafFun). ++ {Tree, NodesAdded} = map_leaves_count(Tree0, LeafFun), ++ {Tree, NodeCount + NodesAdded}. + + %%-------------------------------------------------------------------- +--spec add_leaf_siblings(policy_tree(), SiblingFun) -> policy_tree() when ++-spec add_leaf_siblings(policy_tree(), non_neg_integer(), SiblingFun) -> ++ {policy_tree(), non_neg_integer()} when + SiblingFun ::fun((policy_tree_node()) -> no_sibling | [policy_node()]). + + %% + %% Add sibling leaves if SiblingFun returns a list of policy nodes + %% for the leaf parent. + %%-------------------------------------------------------------------- +-add_leaf_siblings({Parent,[{_, _}|_] = ChildNodes}, SiblingFun) -> +- {Parent, lists:map(fun(ChildNode)-> +- add_leaf_siblings(ChildNode, SiblingFun) +- end, ChildNodes)}; +-add_leaf_siblings({Parent, Leaves} = Node, SiblingFun) -> ++add_leaf_siblings({Parent,[{_, _}|_] = ChildNodes}, NodeCount, SiblingFun) -> ++ {Leaves, NodesAdded} = ++ lists:mapfoldl(fun(ChildNode, Acc)-> ++ {Leaves, NodesAdded} = ++ add_leaf_siblings(ChildNode, 0, SiblingFun), ++ {Leaves, Acc + NodesAdded} ++ end, 0, ChildNodes), ++ {{Parent, Leaves}, NodeCount + NodesAdded}; ++add_leaf_siblings({Parent, Leaves} = Node, NodeCount, SiblingFun) -> + case SiblingFun(Parent) of + no_sibling -> +- Node; ++ {Node, NodeCount}; + Siblings -> +- {Parent, Leaves ++ Siblings} ++ {{Parent, Leaves ++ Siblings}, NodeCount + length(Siblings)} + end. + + %%-------------------------------------------------------------------- +@@ -318,6 +327,21 @@ valid_policy_node_set(_) -> + %%%=================================================================== + %%% Internal functions + %%%=================================================================== ++map_leaves_count({Parent, [{_, _}|_] = ChildNodes}, LeafFun) -> ++ {Leaves, NodesAdded} = ++ lists:mapfoldl(fun(ChildNode, Acc)-> ++ {Node, Added} = map_leaves_count(ChildNode, LeafFun), ++ {Node, Acc + Added} ++ end, 0, ChildNodes), ++ {{Parent, Leaves}, NodesAdded}; ++map_leaves_count({Parent, Leaves0}, LeafFun) -> ++ {Leaves, NodesAdded} = ++ lists:mapfoldl(fun(L, Acc) -> ++ Node = {_, ChildNodes} = LeafFun(L), ++ {Node, Acc + length(ChildNodes)} ++ end, 0, Leaves0), ++ {{Parent, Leaves}, NodesAdded}. ++ + any_policy_node() -> + policy_node(?anyPolicy, [], [?anyPolicy]). + +diff --git a/lib/public_key/src/public_key.erl b/lib/public_key/src/public_key.erl +index a5e4397433..8f4a6bc8d1 100644 +--- a/lib/public_key/src/public_key.erl ++++ b/lib/public_key/src/public_key.erl +@@ -313,6 +313,7 @@ The reason that a certifcate gets rejected by the certificate path validation. + {key_usage_mismatch, term()} | + {'policy_requirement_not_met', term()} | {'invalid_policy_mapping', term()} | + {revoked, crl_reason()} | invalid_validity_dates | ++ policy_tree_exceeded | + {revocation_status_undetermined, term()} | atom(). + + -doc(#{title => <<"Certificates">>}). +@@ -1666,6 +1667,11 @@ Explanations of reasons for a bad certificate: + - **invalid_validity_dates** - The validity section of the X.509 certificate(s) + contains invalid date formats not matching the RFC. + ++- **policy_tree_exceeded** - The certificate chain's policy tree exceeded the ++ maximum allowed node count. This indicates a malformed or malicious chain with ++ exponentially expanding policy mappings. This error cannot be overridden by the ++ `verify_fun`. ++ + - **atom()** - Application-specific error reason that is to be checked by the + `verify_fun`. + """. +diff --git a/lib/public_key/test/pubkey_policy_tree_SUITE.erl b/lib/public_key/test/pubkey_policy_tree_SUITE.erl +index c152e7567e..b9b48d8532 100644 +--- a/lib/public_key/test/pubkey_policy_tree_SUITE.erl ++++ b/lib/public_key/test/pubkey_policy_tree_SUITE.erl +@@ -1,3 +1,25 @@ ++%% ++%% %CopyrightBegin% ++%% ++%% SPDX-License-Identifier: Apache-2.0 ++%% ++%% Copyright Ericsson AB 2024-2026. 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. ++%% You may obtain a copy of the License at ++%% ++%% http://www.apache.org/licenses/LICENSE-2.0 ++%% ++%% Unless required by applicable law or agreed to in writing, software ++%% distributed under the License is distributed on an "AS IS" BASIS, ++%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++%% See the License for the specific language governing permissions and ++%% limitations under the License. ++%% ++%% %CopyrightEnd% ++%% ++ + -module(pubkey_policy_tree_SUITE). + -compile([export_all, nowarn_export_all]). + +@@ -173,9 +195,9 @@ add_leaves(_Config) -> + (_) -> + [] + end, +- Instructions = [{add_leaves, [AddLeavesFun1]}, +- {add_leaves, [AddLeavesFun1]}, +- {add_leaves, [AddLeavesFun2]}], ++ Instructions = [{add_leaves, 2, [AddLeavesFun1]}, ++ {add_leaves, 4, [AddLeavesFun1]}, ++ {add_leaves, 4, [AddLeavesFun2]}], + {ok, Tree} = explain(RootTree, Instructions), + ?assertEqual({?ROOT_PN, + [{?PN("GOLD"), +@@ -198,16 +220,18 @@ add_leaf_siblings(_Config) -> + (_) -> + [] + end, +- Instructions = [{add_leaf_siblings, [AddLeavesFun1]}, +- {add_leaf_siblings, [AddLeavesFun1]}, +- {add_leaf_siblings, [AddLeavesFun2]} ++ Instructions = [{add_leaf_siblings, 4, [AddLeavesFun1]}, ++ {add_leaf_siblings, 4, [AddLeavesFun1]}, ++ {add_leaf_siblings, 2, [AddLeavesFun2]} + ], + {ok, Tree} = explain(tree_with_any_policy_node1(), Instructions), + ?assertEqual({?ROOT_PN, + [{?PN(?anyPolicy), +- [?PN("GOLD"), ?PN("GOLD"), ?PN("SILVER"), ?PN("GOLD"), ?PN("SILVER"), ?PN("PINK")]}, ++ [?PN("GOLD"), ?PN("GOLD"), ?PN("SILVER"), ?PN("GOLD"), ++ ?PN("SILVER"), ?PN("PINK")]}, + {?PN("SILVER", ["A"]), +- [?PN("SILVER", ["B"]), ?PN("GOLD"), ?PN("SILVER"), ?PN("GOLD"), ?PN("SILVER"), ?PN("PURPLE")]}]}, ++ [?PN("SILVER", ["B"]), ?PN("GOLD"), ?PN("SILVER"), ++ ?PN("GOLD"), ?PN("SILVER"), ?PN("PURPLE")]}]}, + Tree), + ok. + +@@ -222,6 +246,13 @@ explain(InitTree, Instructions) -> + + explain(Tree, [], _) -> + {ok, Tree}; ++explain(Tree0, [{FunctionName, ExpectedTreeGrowth, Args} | Rest], N) -> ++ Title = io_lib:format("~p) pubkey_policy_tree:~p()", [N, FunctionName]), ++ ct:log("=============================================~nSTEP: ~s", [Title]), ++ {Tree, TreeGrowth} = apply(pubkey_policy_tree, FunctionName, [Tree0, 0 | Args]), ++ ?assertEqual(ExpectedTreeGrowth, TreeGrowth), ++ ?PAL_MMD(to_mmd(Title, Tree)), ++ explain(Tree, Rest, N+1); + explain(Tree0, [{FunctionName, Args} | Rest], N) -> + Title = io_lib:format("~p) pubkey_policy_tree:~p()", [N, FunctionName]), + ct:log("=============================================~nSTEP: ~s", [Title]), +diff --git a/lib/ssl/src/ssl_handshake.erl b/lib/ssl/src/ssl_handshake.erl +index 8dcb2578d4..7ef03b1c71 100644 +--- a/lib/ssl/src/ssl_handshake.erl ++++ b/lib/ssl/src/ssl_handshake.erl +@@ -2197,6 +2197,8 @@ path_validation_alert({bad_cert, {ca_invalid_ext_keyusage, ExtKeyUses}}, _, _) - + ?ALERT_REC(?FATAL, ?UNSUPPORTED_CERTIFICATE, {ca_invalid_ext_keyusage, Uses}); + path_validation_alert({bad_cert, {key_usage_mismatch, _} = Reason}, _, _) -> + ?ALERT_REC(?FATAL, ?UNSUPPORTED_CERTIFICATE, Reason); ++path_validation_alert({bad_cert, policy_tree_exceeded}, _, _) -> ++ ?ALERT_REC(?FATAL, ?BAD_CERTIFICATE, policy_tree_exceeded); + path_validation_alert(Reason, _,_) -> + ?ALERT_REC(?FATAL, ?HANDSHAKE_FAILURE, Reason). + +-- +2.47.3 + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/series erlang-27.3.4.1+dfsg/debian/patches/series --- erlang-27.3.4.1+dfsg/debian/patches/series 2026-04-04 13:45:31.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/series 2026-08-22 19:38:30.000000000 +0000 @@ -13,3 +13,33 @@ CVE-2026-23941.patch CVE-2026-23942.patch CVE-2026-23943.patch +CVE-2026-28810.patch +CVE-2026-28808.patch +CVE-2026-32144-1.patch +CVE-2026-32144-2.patch +CVE-2026-32147.patch +CVE-2026-42789.patch +CVE-2026-42790.patch +CVE-2026-42791.patch +CVE-2026-48860.patch +CVE-2026-48858.patch +CVE-2026-49759.patch +CVE-2026-48855.patch +CVE-2026-48856.patch +CVE-2026-49760.patch +CVE-2026-54887.patch +CVE-2026-54891.patch +CVE-2026-55952.patch +CVE-2026-55950.patch +CVE-2026-53422.patch +CVE-2026-54886.patch +CVE-2026-55737.patch +CVE-2026-55953.patch +CVE-2026-58227.patch +CVE-2026-59250.patch +CVE-2026-47078.patch +CVE-2026-42792-1.patch +CVE-2026-42792-2.patch +CVE-2026-54890.patch +CVE-2026-59251.patch +#ssh-test-macros.patch diff -Nru erlang-27.3.4.1+dfsg/debian/patches/ssh-test-macros.patch erlang-27.3.4.1+dfsg/debian/patches/ssh-test-macros.patch --- erlang-27.3.4.1+dfsg/debian/patches/ssh-test-macros.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/ssh-test-macros.patch 2026-08-22 19:38:30.000000000 +0000 @@ -0,0 +1,58 @@ +From 11cb3cec4be450446bda8ecfdcb866ae5703acd1 Mon Sep 17 00:00:00 2001 +From: Debian Erlang Packagers +Date: Thu, 20 Aug 2026 13:11:24 +0800 +Subject: [PATCH] Add ssh test macros needed by the backported CVE regression + tests + +The upstream security fixes backported here bring their own regression tests, +and those tests use test scaffolding that reached maint-27 in unrelated, +non-security commits which are deliberately not backported: + + ?CT_FAIL/2 in ssh_test_lib.hrl, added by 94776b53be ("Add tests for each + side may guess functionality"), upstream merge 08f52f15bc (OTP-19864) + -include_lib("stdlib/include/assert.hrl") in ssh_sftpd_SUITE.erl, added by + 981a866ece ("ssh: fix ssh_sftpd:new_handle"), upstream merge 094f05da81 + (OTP-19691) + +Without them ssh_sftpd_SUITE.erl fails to compile ("undefined macro +'CT_FAIL/2'", "undefined macro 'assertEqual/2'") and the whole ssh test run +aborts. + +Only these two definitions are taken, nothing else from those commits. Note +that upstream's ssh_test_lib.hrl also defines ?v/2, ?v/3 and ?CT_PAL/1,2; +those must NOT be added, because ssh_protocol_SUITE.erl in this tree still +carries its own local ?v definition (upstream dropped it in a commit that is +likewise not backported), so adding ?v breaks that suite with "redefining +macro 'v'". ?CT_PAL is used by no test here. + +Test suites only; no change to any installed code. +--- + lib/ssh/test/ssh_sftpd_SUITE.erl | 1 + + lib/ssh/test/ssh_test_lib.hrl | 2 ++ + 2 files changed, 3 insertions(+) + +diff --git a/lib/ssh/test/ssh_sftpd_SUITE.erl b/lib/ssh/test/ssh_sftpd_SUITE.erl +index 60fea47ff7..2ea70211ff 100644 +--- a/lib/ssh/test/ssh_sftpd_SUITE.erl ++++ b/lib/ssh/test/ssh_sftpd_SUITE.erl +@@ -64,6 +64,7 @@ + + -include_lib("common_test/include/ct.hrl"). + -include_lib("kernel/include/file.hrl"). ++-include_lib("stdlib/include/assert.hrl"). + -include("ssh_xfer.hrl"). + -include("ssh.hrl"). + -include("ssh_test_lib.hrl"). +diff --git a/lib/ssh/test/ssh_test_lib.hrl b/lib/ssh/test/ssh_test_lib.hrl +index 8171b64795..bdddbfbe67 100644 +--- a/lib/ssh/test/ssh_test_lib.hrl ++++ b/lib/ssh/test/ssh_test_lib.hrl +@@ -78,3 +78,5 @@ + ?SSH_TEST_LIB_FORMAT ++ F, + ?SSH_TEST_LIB_ARGS ++ Args, + [esc_chars]))). ++-define(CT_FAIL(F, Args), ++ (ct:fail(?SSH_TEST_LIB_FORMAT ++ F, ?SSH_TEST_LIB_ARGS ++ Args))). +-- +2.47.3 +