Version in base suite: 27.3.4.1+dfsg-1+deb13u3 Base version: erlang_27.3.4.1+dfsg-1+deb13u3 Target version: erlang_27.3.4.1+dfsg-1+deb13u4 Base file: /srv/ftp-master.debian.org/ftp/pool/main/e/erlang/erlang_27.3.4.1+dfsg-1+deb13u3.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/e/erlang/erlang_27.3.4.1+dfsg-1+deb13u4.dsc changelog | 104 +--- patches/CVE-2026-55951.patch | 676 +++++++++++++++++++++++++++++ patches/CVE-2026-59696.patch | 57 ++ patches/CVE-2026-65634.patch | 216 +++++++++ patches/CVE-2026-66357.patch | 84 +++ patches/CVE-2026-68956-1.patch | 780 ++++++++++++++++++++++++++++++++++ patches/CVE-2026-68956-2.patch | 157 ++++++ patches/CVE-2026-68956-3.patch | 190 ++++++++ patches/CVE-2026-68956-4.patch | 166 +++++++ patches/CVE-2026-69664.patch | 145 ++++++ patches/CVE-2026-70399.patch | 286 ++++++++++++ patches/CVE-2026-70405-followup.patch | 631 +++++++++++++++++++++++++++ patches/CVE-2026-70405.patch | 81 +++ patches/CVE-2026-70409.patch | 51 ++ patches/CVE-2026-71380-1.patch | 318 +++++++++++++ patches/CVE-2026-71380-2.patch | 431 ++++++++++++++++++ patches/CVE-2026-71380-3.patch | 39 + patches/CVE-2026-71380-4.patch | 374 ++++++++++++++++ patches/CVE-2026-71380-5.patch | 68 ++ patches/CVE-2026-71380-6.patch | 42 + patches/CVE-2026-73270-1.patch | 222 +++++++++ patches/CVE-2026-73270-2.patch | 36 + patches/CVE-2026-73276-1.patch | 100 ++++ patches/CVE-2026-73276-2.patch | 96 ++++ patches/CVE-2026-73812.patch | 210 +++++++++ patches/CVE-2026-74835-1.patch | 264 +++++++++++ patches/CVE-2026-74835-2.patch | 21 patches/CVE-2026-74994.patch | 158 ++++++ patches/CVE-2026-75538-1.patch | 52 ++ patches/CVE-2026-75538-2.patch | 123 +++++ patches/CVE-2026-89422.patch | 359 +++++++++++++++ patches/series | 32 + patches/ssh-test-macros.patch | 18 33 files changed, 6495 insertions(+), 92 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpey84inw3/erlang_27.3.4.1+dfsg-1+deb13u3.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpey84inw3/erlang_27.3.4.1+dfsg-1+deb13u4.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-08-22 19:38:30.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/changelog 2026-10-20 16:55:29.000000000 +0000 @@ -1,82 +1,34 @@ -erlang (1:27.3.4.1+dfsg-1+deb13u3) trixie-security; urgency=medium +erlang (1:27.3.4.1+dfsg-1+deb13u4) trixie-security; urgency=medium + + * Non-maintainer local preparation (NMU draft); not upload ready + (runtime validation blocked). + * Fixes: CVE-2026-55951, CVE-2026-59696, CVE-2026-65634, CVE-2026-66357, + CVE-2026-66835, CVE-2026-68956, CVE-2026-69664, CVE-2026-70399, + CVE-2026-70405, CVE-2026-70409, CVE-2026-71380, CVE-2026-71562, + CVE-2026-73270, CVE-2026-73276, CVE-2026-73812, CVE-2026-74835, + CVE-2026-74994, CVE-2026-75538, CVE-2026-89422 + * Record CVE-2026-71562 (GHSA-cqx9-9hq6-m8wf) alongside CVE-2026-55951 + (GHSA-f9fw-mg7q-4g3x) for the existing inets/httpc backport + (CVE-2026-55951.patch, upstream OTP-20343); attribution only. + * Enable ssh-test-macros.patch in the patch series for the backported + SSH regression tests; retain only CT_FAIL/2 and the assert.hrl include + from upstream commits 94776b53be and 981a866ece (test scaffolding only). - [ 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. + -- Aron Xu Wed, 21 Oct 2026 00:55:29 +0800 + +erlang (1:27.3.4.1+dfsg-1+deb13u3) trixie-security; urgency=medium - -- Sergei Golovan Sat, 22 Aug 2026 22:38:30 +0300 + * Non-maintainer upload by the Security Team. + * Fixes: CVE-2026-28808, CVE-2026-28810, CVE-2026-32144, CVE-2026-32147, + CVE-2026-42789, CVE-2026-42790, CVE-2026-42791, CVE-2026-42792, + CVE-2026-47078, CVE-2026-48855, CVE-2026-48856, CVE-2026-48858, + CVE-2026-48860, CVE-2026-49759, CVE-2026-49760, CVE-2026-53422, + CVE-2026-54886, CVE-2026-54887, CVE-2026-54890, CVE-2026-54891, + CVE-2026-55737, CVE-2026-55950, CVE-2026-55952, CVE-2026-55953, + CVE-2026-58227, CVE-2026-59250, CVE-2026-59251, + * Closes: #1139727, #1139823, #1141414, #1142985 + -- Aron Xu Thu, 20 Aug 2026 13:21:18 +0800 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-55951.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55951.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55951.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-55951.patch 2026-10-19 04:27:52.000000000 +0000 @@ -0,0 +1,676 @@ +From: Konrad Pietrzak +Date: Thu, 25 Jun 2026 13:51:22 +0200 +Subject: inets,stdlib,eldap: Bound integer parsing to mitigate DoS + +inets (httpc): +- Add per-request max_header_size and max_body_size options +- Enforce cumulative header byte count during parsing; reject with + {error, {header_too_long, {max, MaxSize}}} when exceeded +- Default max_header_size is 10240 (same as httpd) +- Validate Content-Length contains only digits before storing, + preventing badarg crashes on malformed responses +- Update limits from new request options on connection reuse +- Catch validation throws in handler, return clean errors to caller +- Document new options in request/4,5 docstring + +stdlib (uri_string): +- Bound port component digit count in uri_string:parse/1 + +eldap: +- Bound port digit count in referral URL parsing + +snmp: +- Bound INTEGER value to max 32-bit value as in SMI-MIB +- Bound Counter32/Gauge32/TimeTicks to 5 bytes (32-bit + sign) +- Bound Counter64 to 9 bytes (64-bit + sign) + +Mitigates GHSA-8qw4-2chm-mvj2. + +Origin: upstream, https://github.com/erlang/otp/commit/aba0fe8c2d700bf4ac94607cf7f00e53bbe4042d +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/inets/src/http_client/httpc.erl | 74 +++++++-- + lib/inets/src/http_client/httpc_handler.erl | 28 +++- + lib/inets/src/http_client/httpc_response.erl | 162 ++++++++++--------- + lib/inets/src/http_lib/http_response.erl | 10 +- + lib/inets/test/httpc_SUITE.erl | 163 +++++++++++++++++++- + 5 files changed, 333 insertions(+), 104 deletions(-) + +--- a/lib/inets/src/http_client/httpc.erl ++++ b/lib/inets/src/http_client/httpc.erl +@@ -223,7 +223,9 @@ request(Url, Profile) -> + | {headers_as_is, boolean()} + | {socket_opts, [SocketOpt]} + | {receiver, Receiver} +- | {ipv6_host_with_brackets, boolean()}, ++ | {ipv6_host_with_brackets, boolean()} ++ | {max_header_size, MaxSize} ++ | {max_body_size, MaxSize}, + StreamTo :: none | self | {self, once} | file:name_all(), + SocketOpt :: term(), + BodyFormat :: string | binary, +@@ -232,6 +234,7 @@ request(Url, Profile) -> + | { ReceiverModule::atom() + , ReceiverFunction::atom() + , ReceiverArgs::list()}, ++ MaxSize :: non_neg_integer() | nolimit, + Result :: { StatusLine , [HttpHeader], HttpBodyResult} + | { StatusCode, HttpBodyResult} + | RequestId +@@ -407,6 +410,28 @@ Options details: + (`true`) or stripped (`false`). + + Default is `false`. ++ ++ [](){: #opt_max_header_size } ++ ++- **`max_header_size`** - Maximum total number of bytes allowed for the response ++ headers (cumulative across all header lines). If the server sends headers ++ exceeding this limit, the request fails with ++ `{error, {header_too_long, {max, MaxSize}}}`. ++ ++ The atom `nolimit` disables the check (not recommended). ++ ++ Default is `10240` (same as the [`max_header_size`](`m:httpd#prop_max_header_size`) ++ configuration for `httpd`). ++ ++ [](){: #opt_max_body_size } ++ ++- **`max_body_size`** - Maximum number of bytes allowed for the response body. ++ If the `Content-Length` of the response exceeds this limit, the request fails ++ with `{error, body_too_big}`. ++ ++ The atom `nolimit` disables the check. ++ ++ Default is `nolimit`. + """. + -doc(#{since => <<"OTP R13B04">>}). + -spec request(Method, Request, HttpOptions, Options, Profile) -> {ok, Result} | {error, term()} when +@@ -442,7 +467,9 @@ Options details: + | {headers_as_is, boolean()} + | {socket_opts, [SocketOpt]} + | {receiver, Receiver} +- | {ipv6_host_with_brackets, boolean()}, ++ | {ipv6_host_with_brackets, boolean()} ++ | {max_header_size, MaxSize} ++ | {max_body_size, MaxSize}, + StreamTo :: none | self | {self, once} | file:name_all(), + BodyFormat :: string | binary, + SocketOpt :: term(), +@@ -451,6 +478,7 @@ Options details: + | { ReceiverModule::atom() + , ReceiverFunction::atom() + , ReceiverArgs::list()}, ++ MaxSize :: non_neg_integer() | nolimit, + Profile :: atom() | pid(), + HttpVersion :: uri_string:uri_string(), + Result :: {StatusLine +@@ -1327,7 +1355,9 @@ maybe_format_body(BinBody, Options) -> + | {headers_as_is, boolean()} + | {socket_opts, [SocketOpt]} + | {receiver, Receiver} +- | {ipv6_host_with_brackets, boolean()}, ++ | {ipv6_host_with_brackets, boolean()} ++ | {max_header_size, MaxSize} ++ | {max_body_size, MaxSize}, + BodyFormat :: string | binary, + StreamTo :: none | self | {self, once} | file:name_all(), + SocketOpt :: term(), +@@ -1335,7 +1365,8 @@ maybe_format_body(BinBody, Options) -> + | fun((term()) -> term()) + | { ReceiverModule::atom() + , ReceiverFunction::atom() +- , ReceiverArgs::list()}. ++ , ReceiverArgs::list()}, ++ MaxSize :: non_neg_integer() | nolimit. + %% This options is a workaround for http servers that do not follow the + %% http standard and have case sensitive header parsing. Should only be + %% used if there is no other way to communicate with the server or for +@@ -1520,16 +1551,26 @@ request_options_defaults() -> + + VerifyBrackets = VerifyBoolean, + ++ VerifyIntegerNoLimit = ++ fun(nolimit) -> ++ ok; ++ (Value) when ++ is_integer(Value), Value > 0 -> ++ ok ++ end, ++ + [ +- {sync, true, VerifySync}, +- {stream, none, VerifyStream}, +- {body_format, string, VerifyBodyFormat}, +- {full_result, true, VerifyFullResult}, +- {headers_as_is, false, VerifyHeaderAsIs}, +- {receiver, alias(), VerifyReceiver}, +- {socket_opts, undefined, VerifySocketOpts}, +- {ipv6_host_with_brackets, false, VerifyBrackets} +- ]. ++ {sync, true, VerifySync}, ++ {stream, none, VerifyStream}, ++ {body_format, string, VerifyBodyFormat}, ++ {full_result, true, VerifyFullResult}, ++ {headers_as_is, false, VerifyHeaderAsIs}, ++ {receiver, alias(), VerifyReceiver}, ++ {socket_opts, undefined, VerifySocketOpts}, ++ {ipv6_host_with_brackets, false, VerifyBrackets}, ++ {max_header_size, ?HTTP_MAX_HEADER_SIZE, VerifyIntegerNoLimit}, ++ {max_body_size, ?HTTP_MAX_BODY_SIZE, VerifyIntegerNoLimit} ++ ]. + + request_options(Options) -> + Defaults = request_options_defaults(), +@@ -1575,7 +1616,9 @@ request_options([{Key, DefaultVal, Verify} | Defaults], Options, Acc) -> + | {headers_as_is, boolean()} + | {socket_opts, [SocketOpt]} + | {receiver, Receiver} +- | {ipv6_host_with_brackets, boolean()}, ++ | {ipv6_host_with_brackets, boolean()} ++ | {max_header_size, MaxSize} ++ | {max_body_size, MaxSize}, + StreamTo :: none | self | {self, once} | file:name_all(), + BodyFormat :: string | binary, + SocketOpt :: term(), +@@ -1584,7 +1627,8 @@ request_options([{Key, DefaultVal, Verify} | Defaults], Options, Acc) -> + | fun((term()) -> term()) + | { ReceiverModule::atom() + , ReceiverFunction::atom() +- , ReceiverArgs::list()}. ++ , ReceiverArgs::list()}, ++ MaxSize :: non_neg_integer() | nolimit. + request_options_sanity_check(Opts) -> + case proplists:get_value(sync, Opts) of + Sync when (Sync =:= true) -> +--- a/lib/inets/src/http_client/httpc_handler.erl ++++ b/lib/inets/src/http_client/httpc_handler.erl +@@ -227,17 +227,23 @@ init([Parent, Request, Options, ProfileName]) -> + handle_verbose(Options#options.verbose), + ProxyOptions = handle_proxy_options(Request#request.scheme, Options), + Address = handle_proxy(Request#request.address, ProxyOptions), ++ MaxHeaderSize = proplists:get_value(max_header_size, Request#request.request_options), ++ MaxBodySize = proplists:get_value(max_body_size, Request#request.request_options), + {ok, State} = + %% #state.once should initially be 'inactive' because we + %% activate the socket at first regardless of the state. + case {Address /= Request#request.address, Request#request.scheme} of + {true, https} -> + connect_and_send_upgrade_request(Address, Request, +- #state{options = Options, ++ #state{max_header_size = MaxHeaderSize, ++ max_body_size = MaxBodySize, ++ options = Options, + profile_name = ProfileName}); + {_, _} -> + connect_and_send_first_request(Address, Request, +- #state{options = Options, ++ #state{max_header_size = MaxHeaderSize, ++ max_body_size = MaxBodySize, ++ options = Options, + profile_name = ProfileName}) + end, + gen_server:enter_loop(?MODULE, [], State). +@@ -251,6 +257,20 @@ init([Parent, Request, Options, ProfileName]) -> + %% {stop, Reason, State} (terminate/2 is called) + %% Description: Handling call messages + %%-------------------------------------------------------------------- ++handle_call(Request, From, State0) when ++ is_record(Request, request) -> ++ MaxHeaderSize = proplists:get_value(max_header_size, Request#request.request_options), ++ MaxBodySize = proplists:get_value(max_body_size, Request#request.request_options), ++ State = State0#state{max_header_size = MaxHeaderSize, ++ max_body_size = MaxBodySize}, ++ try do_handle_call(Request, From, State) of ++ Result -> ++ Result ++ catch ++ Class:Reason:ST -> ++ {stop, {shutdown, {{Class, Reason}, ST}}, State} ++ end; ++ + handle_call(Request, From, State) -> + try do_handle_call(Request, From, State) of + Result -> +@@ -541,6 +561,10 @@ do_handle_info({Proto, _Socket, Data}, + activate_once(Session), + {noreply, State#state{mfa = NewMFA}} + catch ++ throw:{error, Err}:_ -> ++ ClientErrMsg = httpc_response:error(Request, Err), ++ NewState = answer_request(Request, ClientErrMsg, State), ++ {stop, normal, NewState}; + Class:Reason:ST -> + ClientReason = {could_not_parse_as_http, Data}, + ClientErrMsg = httpc_response:error(Request, ClientReason), +--- a/lib/inets/src/http_client/httpc_response.erl ++++ b/lib/inets/src/http_client/httpc_response.erl +@@ -57,9 +57,9 @@ parse_reason_phrase([Bin, Rest, Phrase, MaxHeaderSize, Result, Relaxed]) -> + parse_reason_phrase(<>, Phrase, + MaxHeaderSize, Result, Relaxed). + +-parse_headers([Bin, Rest,Header, Headers, MaxHeaderSize, Result, Relaxed]) -> ++parse_headers([Bin, Rest,Header, Headers, MaxHeaderSize, CurrentSize, Result, Relaxed]) -> + parse_headers(<>, Header, Headers, +- MaxHeaderSize, Result, Relaxed). ++ MaxHeaderSize, CurrentSize, Result, Relaxed). + + whole_body(Body, Length) when is_binary(Body)-> + case byte_size(Body) of +@@ -196,7 +196,7 @@ parse_status_code(<>, StatusCodeStr, + + parse_status_code(<>, StatusCodeStr, + MaxHeaderSize, Result, true) -> +- parse_headers(Rest, [], [], MaxHeaderSize, ++ parse_headers(Rest, [], [], MaxHeaderSize, 0, + [" ", list_to_integer(lists:reverse( + string:trim(StatusCodeStr))) + | Result], true); +@@ -246,7 +246,7 @@ parse_reason_phrase(<>, Phrase, + MaxHeaderSize, Result, Relaxed); + parse_reason_phrase(<>, Phrase, + MaxHeaderSize, Result, Relaxed) -> +- parse_headers(Rest, [], [], MaxHeaderSize, ++ parse_headers(Rest, [], [], MaxHeaderSize, 0, + [lists:reverse(Phrase) | Result], Relaxed); + parse_reason_phrase(<>, Phrase, MaxHeaderSize, Result, Relaxed) -> + %% If ?CR is is missing RFC2616 section-19.3 +@@ -260,100 +260,96 @@ parse_reason_phrase(<>, Phrase, MaxHeaderSize, Result, + parse_reason_phrase(Rest, [Octet | Phrase], MaxHeaderSize, + Result, Relaxed). + +-parse_headers(<<>>, Header, Headers, MaxHeaderSize, Result, Relaxed) -> +- {?MODULE, parse_headers, [<<>>, Header, Headers, MaxHeaderSize, Result, ++parse_headers(<<>>, Header, Headers, MaxHeaderSize, CurrentSize, Result, Relaxed) -> ++ {?MODULE, parse_headers, [<<>>, Header, Headers, MaxHeaderSize, CurrentSize, Result, + Relaxed]}; + + parse_headers(<>, Header, Headers, +- MaxHeaderSize, Result, Relaxed) -> +- %% If ?CR is is missing RFC2616 section-19.3 ++ MaxHeaderSize, CurrentSize, Result, Relaxed) -> ++ %% If ?CR is is missing RFC2616 section-19.3 + parse_headers(<>, Header, Headers, +- MaxHeaderSize, Result, Relaxed); ++ MaxHeaderSize, CurrentSize, Result, Relaxed); + + parse_headers(<>, Header, Headers, +- MaxHeaderSize, Result, Relaxed) -> +- %% If ?CR is is missing RFC2616 section-19.3 ++ MaxHeaderSize, CurrentSize, Result, Relaxed) -> ++ %% If ?CR is is missing RFC2616 section-19.3 + parse_headers(<>, Header, Headers, +- MaxHeaderSize, Result, Relaxed); ++ MaxHeaderSize, CurrentSize, Result, Relaxed); + + parse_headers(<>, Header, Headers, +- MaxHeaderSize, Result, Relaxed) -> ++ _MaxHeaderSize, _CurrentSize, Result, Relaxed) -> + HTTPHeaders = [lists:reverse(Header) | Headers], +- Length = lists:foldl(fun(H, Acc) -> length(H) + Acc end, +- 0, HTTPHeaders), +- case ((Length =< MaxHeaderSize) or (MaxHeaderSize == nolimit)) of +- true -> +- ResponseHeaderRcord = +- http_response:headers(HTTPHeaders, #http_response_h{}), +- +- %% RFC7230, Section 3.3.3 +- %% If a message is received with both a Transfer-Encoding and a +- %% Content-Length header field, the Transfer-Encoding overrides the +- %% Content-Length. Such a message might indicate an attempt to +- %% perform request smuggling (Section 9.5) or response splitting +- %% (Section 9.4) and ought to be handled as an error. A sender MUST +- %% remove the received Content-Length field prior to forwarding such +- %% a message downstream. +- case ResponseHeaderRcord#http_response_h.'transfer-encoding' of +- undefined -> ++ ResponseHeaderRcord = ++ http_response:headers(HTTPHeaders, #http_response_h{}), ++ ++ %% RFC7230, Section 3.3.3 ++ %% If a message is received with both a Transfer-Encoding and a ++ %% Content-Length header field, the Transfer-Encoding overrides the ++ %% Content-Length. Such a message might indicate an attempt to ++ %% perform request smuggling (Section 9.5) or response splitting ++ %% (Section 9.4) and ought to be handled as an error. A sender MUST ++ %% remove the received Content-Length field prior to forwarding such ++ %% a message downstream. ++ case ResponseHeaderRcord#http_response_h.'transfer-encoding' of ++ undefined -> ++ {ok, list_to_tuple( ++ lists:reverse([Body, ResponseHeaderRcord | Result]))}; ++ Value -> ++ TransferEncoding = string:lowercase(Value), ++ ContentLength = ResponseHeaderRcord#http_response_h.'content-length', ++ if ++ %% Respond without error but remove Content-Length field in relaxed mode ++ (Relaxed =:= true) ++ andalso (TransferEncoding =:= "chunked") ++ andalso (ContentLength =/= "-1") -> ++ ResponseHeaderRcordFixed = ++ ResponseHeaderRcord#http_response_h{'content-length' = "-1"}, ++ {ok, list_to_tuple( ++ lists:reverse([Body, ResponseHeaderRcordFixed | Result]))}; ++ %% Respond with error in default (not relaxed) mode ++ (Relaxed =:= false) ++ andalso (TransferEncoding =:= "chunked") ++ andalso (ContentLength =/= "-1") -> ++ throw({error, {headers_conflict, {'content-length', ++ 'transfer-encoding'}}}); ++ true -> + {ok, list_to_tuple( +- lists:reverse([Body, ResponseHeaderRcord | Result]))}; +- Value -> +- TransferEncoding = string:lowercase(Value), +- ContentLength = ResponseHeaderRcord#http_response_h.'content-length', +- if +- %% Respond without error but remove Content-Length field in relaxed mode +- (Relaxed =:= true) +- andalso (TransferEncoding =:= "chunked") +- andalso (ContentLength =/= "-1") -> +- ResponseHeaderRcordFixed = +- ResponseHeaderRcord#http_response_h{'content-length' = "-1"}, +- {ok, list_to_tuple( +- lists:reverse([Body, ResponseHeaderRcordFixed | Result]))}; +- %% Respond with error in default (not relaxed) mode +- (Relaxed =:= false) +- andalso (TransferEncoding =:= "chunked") +- andalso (ContentLength =/= "-1") -> +- throw({error, {headers_conflict, {'content-length', +- 'transfer-encoding'}}}); +- true -> +- {ok, list_to_tuple( +- lists:reverse([Body, ResponseHeaderRcord | Result]))} +- end +- end; +- false -> +- throw({error, {header_too_long, MaxHeaderSize, +- MaxHeaderSize-Length}}) ++ lists:reverse([Body, ResponseHeaderRcord | Result]))} ++ end + end; +-parse_headers(<> = Data, Header, Headers, +- MaxHeaderSize, Result, Relaxed) -> +- {?MODULE, parse_headers, [Data, Header, Headers, +- MaxHeaderSize, Result, Relaxed]}; +-parse_headers(<> = Data, Header, Headers, +- MaxHeaderSize, Result, Relaxed) -> +- {?MODULE, parse_headers, [Data, Header, Headers, MaxHeaderSize, +- Result, Relaxed]}; ++parse_headers(<> = Data, Header, Headers, ++ MaxHeaderSize, CurrentSize, Result, Relaxed) -> ++ {?MODULE, parse_headers, [Data, Header, Headers, ++ MaxHeaderSize, CurrentSize, Result, Relaxed]}; ++parse_headers(<> = Data, Header, Headers, ++ MaxHeaderSize, CurrentSize, Result, Relaxed) -> ++ {?MODULE, parse_headers, [Data, Header, Headers, MaxHeaderSize, CurrentSize, ++ Result, Relaxed]}; + parse_headers(<>, Header, Headers, +- MaxHeaderSize, Result, Relaxed) -> +- parse_headers(Rest, [Octet], +- [lists:reverse(Header) | Headers], MaxHeaderSize, +- Result, Relaxed); +-parse_headers(<> = Data, Header, Headers, +- MaxHeaderSize, Result, Relaxed) -> +- {?MODULE, parse_headers, [Data, Header, Headers, MaxHeaderSize, +- Result, Relaxed]}; +- +-parse_headers(<>, Header, Headers, +- MaxHeaderSize, Result, Relaxed) -> +- %% If ?CR is is missing RFC2616 section-19.3 +- parse_headers(<>, Header, Headers, +- MaxHeaderSize, Result, Relaxed); ++ MaxHeaderSize, CurrentSize, Result, Relaxed) -> ++ parse_headers(Rest, [Octet], ++ [lists:reverse(Header) | Headers], MaxHeaderSize, CurrentSize + 3, ++ Result, Relaxed); ++parse_headers(<> = Data, Header, Headers, ++ MaxHeaderSize, CurrentSize, Result, Relaxed) -> ++ {?MODULE, parse_headers, [Data, Header, Headers, MaxHeaderSize, CurrentSize, ++ Result, Relaxed]}; ++ ++parse_headers(<>, Header, Headers, ++ MaxHeaderSize, CurrentSize, Result, Relaxed) -> ++ %% If ?CR is is missing RFC2616 section-19.3 ++ parse_headers(<>, Header, Headers, ++ MaxHeaderSize, CurrentSize, Result, Relaxed); + + parse_headers(<>, Header, Headers, +- MaxHeaderSize, Result, Relaxed) -> +- parse_headers(Rest, [Octet | Header], Headers, MaxHeaderSize, +- Result, Relaxed). +- ++ MaxHeaderSize, CurrentSize, Result, Relaxed) ++ when (is_integer(MaxHeaderSize) andalso CurrentSize < MaxHeaderSize); ++ MaxHeaderSize =:= nolimit -> ++ parse_headers(Rest, [Octet | Header], Headers, MaxHeaderSize, ++ CurrentSize + 1, Result, Relaxed); ++parse_headers(<<_Octet, _Rest/binary>>, _UnfinalizedHeader, _Headers, ++ MaxHeaderSize, _CurrentSize, _Result, _Relaxed) -> ++ throw({error, {header_too_long, {max, MaxHeaderSize}}}). + + %% RFC2616, Section 10.1.1 + %% Note: +--- a/lib/inets/src/http_lib/http_response.erl ++++ b/lib/inets/src/http_lib/http_response.erl +@@ -126,7 +126,10 @@ headers("content-encoding", Value, Headers) -> + headers("content-language", Value, Headers) -> + Headers#http_response_h{'content-language' = Value}; + headers("content-length", Value, Headers) -> +- Headers#http_response_h{'content-length' = Value}; ++ case is_valid_content_length(Value) of ++ true -> Headers#http_response_h{'content-length' = Value}; ++ false -> throw({error, {invalid_content_length, Value}}) ++ end; + headers("content-location", Value, Headers) -> + Headers#http_response_h{'content-location' = Value}; + headers("content-md5", Value, Headers) -> +@@ -222,3 +225,8 @@ key_value_tuple(_, undefined) -> + undefined; + key_value_tuple(Key, Value) -> + {Key, Value}. ++ ++is_valid_content_length("") -> ++ false; ++is_valid_content_length(Value) -> ++ lists:all(fun(C) -> C >= $0 andalso C =< $9 end, Value). +--- a/lib/inets/test/httpc_SUITE.erl ++++ b/lib/inets/test/httpc_SUITE.erl +@@ -155,7 +155,7 @@ real_requests_esi() -> + + simulated_unix_socket() -> + [unix_domain_socket, +- invalid_ipfamily_unix_socket]. ++ invalid_ipfamily_unix_socket]. + + only_simulated() -> + [ +@@ -201,7 +201,10 @@ only_simulated() -> + get_space, + delete_no_body, + post_with_content_type, +- stream_fun_server_close ++ stream_fun_server_close, ++ abnormal_header_size, ++ invalid_content_length, ++ abnormal_trailer_size + ]. + + server_closing_connection() -> +@@ -1391,7 +1394,7 @@ headers_conflict_chunked_with_length(Config) when is_list(Config) -> + Request = {url(group_name(Config), "/headers_conflict_chunked_with_length.html", Config), []}, + RequestOpts = proplists:get_value(request_opts, Config, []), + Profile = ?profile(Config), +- {error, {could_not_parse_as_http, _}} = httpc:request(get, Request, ++ {error, {headers_conflict, _}} = httpc:request(get, Request, + [{relaxed, false},?SSL_NO_VERIFY], + RequestOpts, Profile), + {ok,{{_,200,_},_,_}} = httpc:request(get, Request, [{relaxed, true}, ?SSL_NO_VERIFY], +@@ -2103,6 +2106,104 @@ def_ssl_opt(_Config) -> + ok. + + %%------------------------------------------------------------------------- ++abnormal_header_size() -> ++ [{doc, "httpc has two options to protect against unreasonable amounts of" ++ "data to be parsed; namely max_header_size and max_body_size which" ++ "default to 10240 and nolimit"}]. ++abnormal_header_size(Config) -> ++ RequestNormal = {url(group_name(Config), "/normal_header_size_restricted.html", Config), []}, ++ RequestAbnormal = {url(group_name(Config), "/abnormal_header_size.html", Config), []}, ++ RequestBoundary = {url(group_name(Config), "/boundary_header_size.html", Config), []}, ++ RequestNormalBigBody = {url(group_name(Config), "/nomal_header_size_big_body.html", Config), []}, ++ Profile = ?profile(Config), ++ ++ %% Normal response passes with explicit max_header_size ++ {ok, {{_, 200, _}, _Headers, _Body}} = ++ httpc:request(get, RequestNormal, ++ [?SSL_NO_VERIFY], ++ [{max_header_size, 4096}], ++ Profile), ++ %% Rejected when max_header_size is artificially low ++ {error, {header_too_long, {max, 16}}} = ++ httpc:request(get, RequestNormal, ++ [?SSL_NO_VERIFY], ++ [{max_header_size, 16}], ++ Profile), ++ %% Abnormal response (>10240 total header bytes) rejected with default limit ++ {error, {header_too_long, {max, ?HTTP_MAX_HEADER_SIZE}}} = ++ httpc:request(get, RequestAbnormal, ++ [?SSL_NO_VERIFY], ++ [], ++ Profile), ++ %% Boundary: one byte under limit fails, exactly at limit passes ++ {error, {header_too_long, _}} = ++ httpc:request(get, RequestBoundary, ++ [?SSL_NO_VERIFY], ++ [{max_header_size, 135}], ++ Profile), ++ {ok, {{_, 200, _}, _, _}} = ++ httpc:request(get, RequestBoundary, ++ [?SSL_NO_VERIFY], ++ [{max_header_size, 136}], ++ Profile), ++ ++ %% max_body_size rejects oversized body ++ {error, body_too_big} = ++ httpc:request(get, RequestNormalBigBody, ++ [?SSL_NO_VERIFY], ++ [{max_body_size, 1000}], ++ Profile), ++ ++%% nolimit bypasses the check (backward compatibility) ++ {ok, {{_, 200, _}, _, _}} = ++ httpc:request(get, RequestAbnormal, ++ [?SSL_NO_VERIFY], ++ [{max_header_size, nolimit}, ++ {body_format, binary}], ++ Profile), ++ ok. ++ ++%%------------------------------------------------------------------------- ++invalid_content_length() -> ++ [{doc, "Malformed Content-Length value is rejected during header parsing"}]. ++invalid_content_length(Config) -> ++ Request = {url(group_name(Config), "/invalid_content_length.html", Config), ++ [{"connection", "close"}]}, ++ Profile = ?profile(Config), ++ {error, {invalid_content_length, _}} = ++ httpc:request(get, Request, [?SSL_NO_VERIFY], [], Profile), ++ ok. ++ ++%%------------------------------------------------------------------------- ++abnormal_trailer_size() -> ++ [{doc, "Chunked trailers are bounded by the same max_header_size option"}]. ++abnormal_trailer_size(Config) -> ++ RequestNormal = {url(group_name(Config), "/chunked_trailer_normal.html", Config), []}, ++ RequestAbnormal = {url(group_name(Config), "/chunked_trailer_abnormal.html", Config), []}, ++ Profile = ?profile(Config), ++ ++ %% Normal chunked response with small trailers passes ++ {ok, {{_, 200, _}, _Headers, _Body}} = ++ httpc:request(get, RequestNormal, ++ [?SSL_NO_VERIFY], ++ [], ++ Profile), ++ ++ %% Chunked response with oversized trailers rejected ++ {error, {header_too_long, {max, _}}} = ++ httpc:request(get, RequestAbnormal, ++ [?SSL_NO_VERIFY], ++ [{max_header_size, 64}], ++ Profile), ++ ++ %% Same oversized trailers pass with nolimit ++ {ok, {{_, 200, _}, _, _}} = ++ httpc:request(get, RequestAbnormal, ++ [?SSL_NO_VERIFY], ++ [{max_header_size, nolimit}], ++ Profile), ++ ok. ++ + remote_socket_close_parallel() -> + [{doc, + "Verify remote socket closure (related tickets: OTP-18509, OTP-18545," +@@ -3074,6 +3175,62 @@ handle_uri(_,"/capture_sensitive_redirect_target.html",_,Headers,_,_) -> + "X-Received-Referer:" ++ Present("referer") ++ "\r\n" ++ + "X-Received-Origin:" ++ Present("origin") ++ "\r\n" ++ + "\r\n"; ++handle_uri(_, "/normal_header_size_restricted.html", _, _, _, _) -> ++ "HTTP/1.1 200 OK\r\n" ++ ++ "Content-Length:150\r\n" ++ ++ "Connection:close\r\n" ++ ++ "\r\n" ++ ++ lists:flatten(["OK " || _ <- lists:seq(1,50)]); ++ ++handle_uri(_, "/boundary_header_size.html", _, _, _, _) -> ++ "HTTP/1.1 200 OK\r\n" ++ ++ "Content-Length:2\r\n" ++ ++ "Connection:close\r\n" ++ ++ "X-Pad:" ++ lists:duplicate(94, $A) ++ "\r\n" ++ ++ "\r\n" ++ ++ "OK"; ++ ++handle_uri(_, "/abnormal_header_size.html", _, _, _, _) -> ++ "HTTP/1.1 200 OK\r\n" ++ ++ "Content-Length:23\r\n" ++ ++ "X-Pad:" ++ lists:duplicate(10241, $0) ++ "\r\n" ++ ++ "Connection: close\r\n" ++ ++ "\r\n" ++ ++ "This should not be read"; ++handle_uri(_, "/nomal_header_size_big_body.html", _, _, _, _) -> ++ "HTTP/1.1 200 OK\r\n" ++ ++ "Content-Length:20000\r\n" ++ ++ "Connection:close\r\n" ++ ++ "\r\n" ++ ++ lists:duplicate(20000, $A); ++ ++handle_uri(_, "/invalid_content_length.html", _, _, _, _) -> ++ "HTTP/1.1 200 OK\r\n" ++ ++ "Content-Length:abc\r\n" ++ ++ "Connection:close\r\n" ++ ++ "\r\n" ++ ++ "body"; ++ ++handle_uri(_, "/chunked_trailer_normal.html", _, _, _, _) -> ++ "HTTP/1.1 200 OK\r\n" ++ ++ "Transfer-Encoding: chunked\r\n" ++ ++ "Connection:close\r\n" ++ ++ "\r\n" ++ ++ "5\r\nhello\r\n" ++ ++ "0\r\n" ++ ++ "X-Checksum: abc123\r\n" ++ ++ "\r\n"; ++ ++handle_uri(_, "/chunked_trailer_abnormal.html", _, _, _, _) -> ++ "HTTP/1.1 200 OK\r\n" ++ ++ "Transfer-Encoding: chunked\r\n" ++ ++ "Connection:close\r\n" ++ ++ "\r\n" ++ ++ "5\r\nhello\r\n" ++ ++ "0\r\n" ++ ++ "X-Huge:" ++ lists:duplicate(200, $B) ++ "\r\n" ++ ++ "\r\n"; ++ + handle_uri(_,_,_,_,_,DefaultResponse) -> + DefaultResponse. + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-59696.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-59696.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-59696.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-59696.patch 2026-09-30 10:13:04.000000000 +0000 @@ -0,0 +1,57 @@ +From: Konrad Pietrzak +Date: Thu, 25 Jun 2026 13:51:22 +0200 +Subject: inets,stdlib,eldap: Bound integer parsing to mitigate DoS + +inets (httpc): +- Add per-request max_header_size and max_body_size options +- Enforce cumulative header byte count during parsing; reject with + {error, {header_too_long, {max, MaxSize}}} when exceeded +- Default max_header_size is 10240 (same as httpd) +- Validate Content-Length contains only digits before storing, + preventing badarg crashes on malformed responses +- Update limits from new request options on connection reuse +- Catch validation throws in handler, return clean errors to caller +- Document new options in request/4,5 docstring + +stdlib (uri_string): +- Bound port component digit count in uri_string:parse/1 + +eldap: +- Bound port digit count in referral URL parsing + +snmp: +- Bound INTEGER value to max 32-bit value as in SMI-MIB +- Bound Counter32/Gauge32/TimeTicks to 5 bytes (32-bit + sign) +- Bound Counter64 to 9 bytes (64-bit + sign) + +Mitigates GHSA-8qw4-2chm-mvj2. + +Origin: upstream, https://github.com/erlang/otp/commit/aba0fe8c2d700bf4ac94607cf7f00e53bbe4042d +Bug-Debian: https://bugs.debian.org/1146603 +Comment: Only the lib/stdlib/src/uri_string.erl hunk (OTP-20344, CVE-2026-59696, + GHSA-8qw4-2chm-mvj2) is taken here. The inets/httpc (CVE-2026-55951), + eldap (CVE-2026-70409) and snmp (CVE-2026-70405) parts of the same + upstream commit are handled by their own CVE patches. +--- + lib/stdlib/src/uri_string.erl | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +--- a/lib/stdlib/src/uri_string.erl ++++ b/lib/stdlib/src/uri_string.erl +@@ -1808,12 +1808,14 @@ calculate_parsed_query_fragment(Input, Unparsed) -> + + get_port(<<>>) -> + undefined; +-get_port(B) -> ++get_port(B) when byte_size(B) =< 5 -> + try binary_to_integer(B) + catch + error:badarg -> + throw({error, invalid_uri, B}) +- end. ++ end; ++get_port(B) -> ++ throw({error, invalid_uri, B}). + + + %% Strip last char if it is in list diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-65634.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-65634.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-65634.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-65634.patch 2026-09-30 10:13:04.000000000 +0000 @@ -0,0 +1,216 @@ +From: John Högberg +Date: Tue, 28 Jul 2026 11:56:41 +0200 +Subject: asn1: Mitigate a DoS vector + +Origin: upstream, https://github.com/erlang/otp/commit/0fe2c02fdc06fab1c63be9db1a7456993d20f838 +--- + lib/asn1/src/asn1rtt_ber.erl | 51 ++++++++++++-------- + lib/asn1/src/asn1rtt_jer.erl | 13 +++-- + lib/asn1/src/asn1rtt_per_common.erl | 24 ++++++--- + lib/asn1/test/testPrim.erl | 23 ++++++++- + 4 files changed, 78 insertions(+), 33 deletions(-) + +--- a/lib/asn1/src/asn1rtt_ber.erl ++++ b/lib/asn1/src/asn1rtt_ber.erl +@@ -548,21 +548,20 @@ decode_tag_and_length(<> = RestBuffer, + {Form, (Class bsl 16) bor TagNo, V, RestBuffer2}; + decode_tag_and_length(<>) -> +- {TagNo, Buffer1} = decode_tag(Buffer, 0), ++ {TagNo, Buffer1} = decode_tag(Buffer), + {Length, RestBuffer} = decode_length(Buffer1), +- << V:Length/binary, RestBuffer2/binary>> = RestBuffer, ++ <> = RestBuffer, + {Form, (Class bsl 16) bor TagNo, V, RestBuffer2}. + ++decode_tag(Buffer) -> ++ decode_tag(Buffer, 0, 0). + +- +-%% last partial tag +-decode_tag(<<0:1,PartialTag:7, Buffer/binary>>, TagAck) -> ++decode_tag(<<0:1, PartialTag:7, Buffer/binary>>, N, TagAck) when N < 32 -> + TagNo = (TagAck bsl 7) bor PartialTag, + {TagNo, Buffer}; +-%% more tags +-decode_tag(<<_:1,PartialTag:7, Buffer/binary>>, TagAck) -> ++decode_tag(<<_:1, PartialTag:7, Buffer/binary>>, N, TagAck) when N < 32 -> + TagAck1 = (TagAck bsl 7) bor PartialTag, +- decode_tag(Buffer, TagAck1). ++ decode_tag(Buffer, N + 1, TagAck1). + + %%======================================================================= + %% +@@ -1184,7 +1183,7 @@ mk_object_val(Val, Ack, Len) -> + + decode_object_identifier(Tlv, Tags) -> + Val = match_tags(Tlv, Tags), +- [AddedObjVal|ObjVals] = dec_subidentifiers(Val,0,[]), ++ [AddedObjVal|ObjVals] = dec_subidentifiers(Val), + {Val1, Val2} = if + AddedObjVal < 40 -> + {0, AddedObjVal}; +@@ -1195,12 +1194,20 @@ decode_object_identifier(Tlv, Tags) -> + end, + list_to_tuple([Val1, Val2 | ObjVals]). + +-dec_subidentifiers(<<>>,_Av,Al) -> +- lists:reverse(Al); +-dec_subidentifiers(<<1:1,H:7,T/binary>>,Av,Al) -> +- dec_subidentifiers(T,(Av bsl 7) + H,Al); +-dec_subidentifiers(<>,Av,Al) -> +- dec_subidentifiers(T,0,[((Av bsl 7) + H)|Al]). ++dec_subidentifiers(<>) -> ++ dec_subidentifiers_1(Octets, 0, 0). ++ ++%% Reject overlong OID components to mitigate a DoS vector. This should be ++%% enough bits for all legitimate uses until someone can prove otherwise. For ++%% comparison, golang limits to 30 bits per component. ++dec_subidentifiers_1(<<1:1, H:7, T/binary>>, N, Av0) when N < 16 -> ++ Av = (Av0 bsl 7) bor H, ++ dec_subidentifiers_1(T, N + 1, Av); ++dec_subidentifiers_1(<>, N, Av0) when N < 16 -> ++ Av = (Av0 bsl 7) bor H, ++ [Av | dec_subidentifiers_1(T, 0, 0)]; ++dec_subidentifiers_1(<<>>, _N, _Av) -> ++ []. + + %%============================================================================ + %% RELATIVE-OID, ITU_T X.690 Chapter 8.20 +@@ -1227,7 +1234,7 @@ enc_relative_oid(Val) -> + %%============================================================================ + decode_relative_oid(Tlv, Tags) -> + Val = match_tags(Tlv, Tags), +- ObjVals = dec_subidentifiers(Val,0,[]), ++ ObjVals = dec_subidentifiers(Val), + list_to_tuple(ObjVals). + + %%============================================================================ +@@ -1423,19 +1430,21 @@ dynamicsort_SETOF(ListOfEncVal) -> + + %% multiple octet tag + dynsort_decode_tag(<>) -> +- TagNum = dynsort_decode_tag(Buffer, 0), ++ TagNum = dynsort_decode_tag_1(Buffer, 0, 0), + {Class,TagNum}; + + %% single tag (< 31 tags) + dynsort_decode_tag(<>) -> + {Class,TagNum}. + +-dynsort_decode_tag(<<0:1,PartialTag:7,_/binary>>, TagAcc) -> ++%% Reject overlong tags to mitigate a DoS vector. This should be enough bits ++%% for all legitimate uses until someone can prove otherwise. ++dynsort_decode_tag_1(<<0:1,PartialTag:7,_/binary>>, N, TagAcc) when N < 16 -> + (TagAcc bsl 7) bor PartialTag; +-dynsort_decode_tag(<<_:1,PartialTag:7,Buffer/binary>>, TagAcc0) -> ++dynsort_decode_tag_1(<<_:1,PartialTag:7,Buffer/binary>>, N, TagAcc0) ++ when N < 16 -> + TagAcc = (TagAcc0 bsl 7) bor PartialTag, +- dynsort_decode_tag(Buffer, TagAcc). +- ++ dynsort_decode_tag_1(Buffer, N + 1, TagAcc). + + %%------------------------------------------------------------------------- + %% INTERNAL HELPER FUNCTIONS (not exported) +--- a/lib/asn1/src/asn1rtt_jer.erl ++++ b/lib/asn1/src/asn1rtt_jer.erl +@@ -416,9 +416,16 @@ oid2json([],Acc) -> + list_to_binary(lists:reverse(Acc)). + + json2oid(OidStr) when is_binary(OidStr) -> +- OidList = binary:split(OidStr,[<<".">>],[global]), +- OidNumList = [binary_to_integer(Num)||Num <- OidList], +- list_to_tuple(OidNumList). ++ SubIds = binary:split(OidStr,[<<".">>],[global]), ++ list_to_tuple(json2oid_1(SubIds)). ++ ++%% Reject overlong OID components to mitigate a DoS vector. This should be ++%% enough bits for all legitimate uses until someone can prove otherwise. For ++%% comparison, golang limits to 30 bits per component. ++json2oid_1([SubId | SubIds]) when byte_size(SubId) < 32 -> ++ [binary_to_integer(SubId) | json2oid_1(SubIds)]; ++json2oid_1([]) -> ++ []. + + jer_bit_str2bitstr(Compact = {_Unused,_Binary}, _NamedBitList) -> + jer_compact2bitstr(Compact); +--- a/lib/asn1/src/asn1rtt_per_common.erl ++++ b/lib/asn1/src/asn1rtt_per_common.erl +@@ -90,7 +90,7 @@ decode_big_chars(Val, N) -> + decode_big_chars_1(decode_chars(Val, N)). + + decode_oid(Octets) -> +- [First|Rest] = dec_subidentifiers(Octets, 0, []), ++ [First|Rest] = dec_subidentifiers(Octets), + Idlist = if + First < 40 -> + [0,First|Rest]; +@@ -102,7 +102,7 @@ decode_oid(Octets) -> + list_to_tuple(Idlist). + + decode_relative_oid(Octets) -> +- list_to_tuple(dec_subidentifiers(Octets, 0, [])). ++ list_to_tuple(dec_subidentifiers(Octets)). + + encode_chars(Val, NumBits) -> + << <> || C <- Val >>. +@@ -372,12 +372,20 @@ decode_big_chars_1([H|T]) -> + [list_to_tuple(binary_to_list(<>))|decode_big_chars_1(T)]; + decode_big_chars_1([]) -> []. + +-dec_subidentifiers([H|T], Av, Al) when H >=16#80 -> +- dec_subidentifiers(T, (Av bsl 7) bor (H band 16#7F), Al); +-dec_subidentifiers([H|T], Av, Al) -> +- dec_subidentifiers(T, 0, [(Av bsl 7) bor H|Al]); +-dec_subidentifiers([], _Av, Al) -> +- lists:reverse(Al). ++dec_subidentifiers(Octets) -> ++ dec_subidentifiers_1(Octets, 0, 0). ++ ++%% Reject overlong OID components to mitigate a DoS vector. This should be ++%% enough bits for all legitimate uses until someone can prove otherwise. For ++%% comparison, golang limits to 30 bits per component. ++dec_subidentifiers_1([H | T], N, Av0) when H >= 16#80, N < 16 -> ++ Av = (Av0 bsl 7) bor (H band 16#7F), ++ dec_subidentifiers_1(T, N + 1, Av); ++dec_subidentifiers_1([H | T], N, Av0) when N < 16 -> ++ Av = (Av0 bsl 7) bor H, ++ [Av | dec_subidentifiers_1(T, 0, 0)]; ++dec_subidentifiers_1([], _N, _Av) -> ++ []. + + enc_char(C0, Lb, Tab) -> + try element(C0-Lb, Tab) of +--- a/lib/asn1/test/testPrim.erl ++++ b/lib/asn1/test/testPrim.erl +@@ -163,7 +163,28 @@ obj_id(_) -> + %%========================================================== + + [roundtrip('ObjId', V) || +- V <- [{0,22,3},{1,39,3},{2,100,3},{2,16303,3},{2,16304,3}]], ++ V <- [{0,22,3}, ++ {1,39,3}, ++ {2,100,3}, ++ {2,16303,3}, ++ {2,16304,3}, ++ {2,1 bsl (14 * 7),3}]], ++ ++ try ++ [roundtrip('ObjId', V) || ++ V <- [{0,22,3}, ++ {1,39,3}, ++ {2,100,3}, ++ {2,16303,3}, ++ {2,16304,3}, ++ {2,1 bsl (20 * 7),3}]] ++ of ++ [_|_]=Res -> ct:fail("Failed to reject overlong oid ~p", [Res]) ++ catch ++ _:_ -> ++ ok ++ end, ++ + ok. + + rel_oid(_Rules) -> diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-66357.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-66357.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-66357.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-66357.patch 2026-09-30 10:13:04.000000000 +0000 @@ -0,0 +1,84 @@ +From: Konrad Pietrzak +Date: Mon, 27 Jul 2026 14:50:42 +0200 +Subject: inets: Reject obs-fold header continuation in httpd (RFC 9112) + +A line starting with SP or HTAB after CRLF in HTTP headers (obs-fold) +was parsed as an independent header by httpd. The key_value/1 helper +strips leading whitespace, promoting the continuation content to a +standalone header. This enables CL.TE request smuggling when httpd +sits behind a folding-aware reverse proxy. + +RFC 9112 Section 5.2 requires that a server which does not support +obs-fold MUST reject such requests with 400 Bad Request. + +Add a clause in parse_headers/7 that detects obs-fold (SP/HTAB after +CRLF) and returns {error, {bad_request, 400, _}, Version}. Add a +matching error handler in httpd_request_handler to send the 400 +response to the client. + +Origin: upstream, https://github.com/erlang/otp/commit/220d618d2479a7180b4a06d0c5aacfaa4af4a85b +Comment: The httpd_request_handler error clause mentioned above is not part + of this commit; the generic {bad_request, Code, Reason} handler is already + present via CVE-2026-23941.patch. +--- + lib/inets/src/http_server/httpd_request.erl | 5 ++++ + lib/inets/test/httpd_SUITE.erl | 27 ++++++++++++++++++++ + 2 files changed, 32 insertions(+) + +--- a/lib/inets/src/http_server/httpd_request.erl ++++ b/lib/inets/src/http_server/httpd_request.erl +@@ -254,6 +254,11 @@ parse_headers(<>, Header, Headers, Current, Max, + %% If ?CR is is missing RFC2616 section-19.3 + parse_headers(<>, Header, Headers, Current, Max, + Options, Result); ++parse_headers(<>, _, _, _, _, _, Result) when ++ Obs =:= ?SP; ++ Obs =:= ?TAB -> ++ HttpVersion = lists:nth(3, lists:reverse(Result)), ++ {error, {bad_request, 400, "obs-fold not supported"}, HttpVersion}; + parse_headers(<>, Header, Headers, Current, Max, + Options, Result) -> + case http_request:key_value(lists:reverse(Header)) of +--- a/lib/inets/test/httpd_SUITE.erl ++++ b/lib/inets/test/httpd_SUITE.erl +@@ -178,6 +178,7 @@ http_get() -> + max_header, + max_content_length, + ignore_invalid_header, ++ reject_obs_fold, + ipv6, + same_file_name_dir_name + ]. +@@ -1765,6 +1766,32 @@ ignore_invalid_header(Config) when is_list(Config) -> + {ok,{{_,204,_}, _, _}} + = httpc:request(get, {Url, Header}, [{timeout, 45000} | Opts], [{headers_as_is, true}]). + ++%%------------------------------------------------------------------------- ++reject_obs_fold() -> ++ [{doc, "RFC 9112 Section 5.2 - obs-fold MUST be rejected with 400"}]. ++reject_obs_fold(Config) when is_list(Config) -> ++ Version = proplists:get_value(http_version, Config), ++ Host = proplists:get_value(host, Config), ++ Port = proplists:get_value(port, Config), ++ Type = proplists:get_value(type, Config), ++ Node = proplists:get_value(node, Config), ++ %% SP obs-fold: line starting with space after CRLF ++ ok = httpd_test_lib:verify_request( ++ Type, Host, Port, Node, ++ "GET /index.html " ++ Version ++ "\r\n" ++ ++ "Host:" ++ Host ++ "\r\n" ++ ++ "X-Foo: bar\r\n" ++ ++ " Transfer-Encoding: chunked\r\n\r\n", ++ [{statuscode, 400}, {version, Version}]), ++ %% HTAB obs-fold: line starting with tab after CRLF ++ ok = httpd_test_lib:verify_request( ++ Type, Host, Port, Node, ++ "GET /index.html " ++ Version ++ "\r\n" ++ ++ "Host:" ++ Host ++ "\r\n" ++ ++ "X-Foo: bar\r\n" ++ ++ "\tX-Injected: evil\r\n\r\n", ++ [{statuscode, 400}, {version, Version}]). ++ + %%------------------------------------------------------------------------- + security_1_1(Config) when is_list(Config) -> + security([{http_version, "HTTP/1.1"} | Config]). diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-1.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-1.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-1.patch 2026-09-30 10:13:04.000000000 +0000 @@ -0,0 +1,780 @@ +From: Michał Wąsowski +Date: Mon, 31 Aug 2026 19:01:07 +0200 +Subject: ssh: Limit size of channel cache entries + +Origin: upstream, https://github.com/erlang/otp/commit/e2bbad0107bba41491a4d549686fd023db8740da +--- + lib/ssh/src/ssh.hrl | 5 +- + lib/ssh/src/ssh_channel_sup.erl | 22 +-- + lib/ssh/src/ssh_client_channel.erl | 19 ++- + lib/ssh/src/ssh_connection.erl | 127 +++++++++------- + lib/ssh/src/ssh_connection_handler.erl | 103 ++++++++----- + lib/ssh/src/ssh_connection_sup.erl | 6 +- + lib/ssh/src/ssh_tcpip_forward_acceptor.erl | 5 +- + lib/ssh/test/ssh_connection_SUITE.erl | 153 ++++++++++++++++++-- + lib/ssh/test/ssh_options_SUITE.erl | 3 +- + 9 files changed, 313 insertions(+), 130 deletions(-) + +--- a/lib/ssh/src/ssh.hrl ++++ b/lib/ssh/src/ssh.hrl +@@ -1105,8 +1105,9 @@ in the User's Guide chapter. + By default, this option is not set. This means that the number is not limited. + + - **`max_channels`{: #hardening_daemon_options-max_channels }** - The maximum +- number of channels with active remote subsystem that are accepted for each +- connection to this daemon ++ number of channels that are accepted for each connection to this daemon. This ++ includes channels that are automatically opened when a port forwarding request ++ is serviced by the server. + + By default, this option is not set. This means that the number is not limited. + +--- a/lib/ssh/src/ssh_channel_sup.erl ++++ b/lib/ssh/src/ssh_channel_sup.erl +@@ -26,9 +26,8 @@ + -moduledoc false. + + -behaviour(supervisor). +--include("ssh.hrl"). + +--export([start_link/1, start_child/8]). ++-export([start_link/1, start_child/7]). + + %% Supervisor callback + -export([init/1]). +@@ -40,16 +39,11 @@ start_link(Args) -> + supervisor:start_link(?MODULE, [Args]). + + +-start_child(client, ChannelSup, ConnRef, Callback, Id, Args, Exec, _Opts) when is_pid(ConnRef) -> ++start_child(client, ChannelSup, ConnRef, Callback, Id, Args, Exec) when is_pid(ConnRef) -> + start_the_channel(ssh_client_channel, ChannelSup, ConnRef, Callback, Id, Args, Exec); + +-start_child(server, ChannelSup, ConnRef, Callback, Id, Args, Exec, Opts) when is_pid(ConnRef) -> +- case max_num_channels_not_exceeded(ChannelSup, Opts) of +- true -> +- start_the_channel(ssh_server_channel, ChannelSup, ConnRef, Callback, Id, Args, Exec); +- false -> +- {error, max_num_channels_exceeded} +- end. ++start_child(server, ChannelSup, ConnRef, Callback, Id, Args, Exec) when is_pid(ConnRef) -> ++ start_the_channel(ssh_server_channel, ChannelSup, ConnRef, Callback, Id, Args, Exec). + + + %%%========================================================================= +@@ -66,14 +60,6 @@ init(_Args) -> + %%%========================================================================= + %%% Internal functions + %%%========================================================================= +-max_num_channels_not_exceeded(ChannelSup, Opts) -> +- MaxNumChannels = ?GET_OPT(max_channels, Opts), +- NumChannels = length([x || {_,_,worker,[ssh_server_channel]} <- +- supervisor:which_children(ChannelSup)]), +- %% Note that NumChannels is BEFORE starting a new one +- NumChannels < MaxNumChannels. +- +- + start_the_channel(ChanMod, ChannelSup, ConnRef, Callback, Id, Args, Exec) -> + ChildSpec = + #{id => make_ref(), +--- a/lib/ssh/src/ssh_client_channel.erl ++++ b/lib/ssh/src/ssh_client_channel.erl +@@ -182,7 +182,7 @@ The following message is taken care of by the `ssh_client_channel` behavior. + terminate/2, code_change/3]). + + %% Internal application API +--export([cache_create/0, cache_lookup/2, cache_update/2, ++-export([cache_create/0, cache_lookup/2, cache_insert/3, cache_update/2, + cache_delete/1, cache_delete/2, cache_foldl/3, + cache_info/2, get_print_info/1, get_print_info/2 + ]). +@@ -572,6 +572,21 @@ cache_lookup(Cache, Key) -> + undefined + end. + ++-doc false. ++cache_insert(Cache, #channel{local_id = Id} = Entry, infinity) when Id =/= undefined -> ++ true = ets:insert_new(Cache, Entry), ++ ok; ++cache_insert(Cache, #channel{local_id = Id} = Entry, Limit) when Id =/= undefined, ++ is_integer(Limit), ++ Limit > 0 -> ++ case cache_info(num_entries, Cache) of ++ Num when Num >= Limit -> ++ {error, max_num_channels_exceeded}; ++ _Num -> ++ true = ets:insert_new(Cache, Entry), ++ ok ++ end. ++ + -doc false. + cache_update(Cache, #channel{local_id = Id} = Entry) when Id =/= undefined -> + ets:insert(Cache, Entry). +@@ -590,7 +605,7 @@ cache_foldl(Fun, Acc, Cache) -> + + -doc false. + cache_info(num_entries, Cache) -> +- proplists:get_value(size, ets:info(Cache)). ++ ets:info(Cache, size). + + -doc false. + get_print_info(Pid) -> +--- a/lib/ssh/src/ssh_connection.erl ++++ b/lib/ssh/src/ssh_connection.erl +@@ -897,8 +897,9 @@ handle_msg(#ssh_msg_channel_open{channel_type = "session" = Type, + + if + MinAcceptedPackSz =< PacketSz -> +- try setup_session(Connection0, RemoteId, +- Type, WindowSz, PacketSz) of ++ Limit = ?GET_OPT(max_channels, SSHopts), ++ try setup_session(Connection0, RemoteId, ++ Type, WindowSz, PacketSz, Limit) of + Result -> + Result + catch _:_ -> +@@ -928,7 +929,6 @@ handle_msg(#ssh_msg_channel_open{channel_type = "forwarded-tcpip", + channel_id_seed = ChId, + suggest_window_size = WinSz, + suggest_packet_size = PktSz, +- options = Options, + connection_supervisor = ConnectionSup + } = C, + client, _SSH) -> +@@ -938,20 +938,20 @@ handle_msg(#ssh_msg_channel_open{channel_type = "forwarded-tcpip", + case gen_tcp:connect(ConnectToHost, ConnectToPort, [{active,false}, binary]) of + {ok,Sock} -> + {ok,Pid} = ssh_connection_sup:start_channel(client, ConnectionSup, self(), +- ssh_tcpip_forward_client, ChId, +- [Sock], undefined, Options), +- ssh_client_channel:cache_update(Cache, +- #channel{type = "forwarded-tcpip", +- sys = "none", +- local_id = ChId, +- remote_id = RemoteId, +- user = Pid, +- recv_window_size = WinSz, +- recv_packet_size = PktSz, +- send_window_size = WindowSize, +- send_packet_size = PacketSize, +- send_buf = queue:new() +- }), ++ ssh_tcpip_forward_client, ChId, ++ [Sock], undefined), ++ Channel = #channel{type = "forwarded-tcpip", ++ sys = "none", ++ local_id = ChId, ++ remote_id = RemoteId, ++ user = Pid, ++ recv_window_size = WinSz, ++ recv_packet_size = PktSz, ++ send_window_size = WindowSize, ++ send_packet_size = PacketSize, ++ send_buf = queue:new() ++ }, ++ ok = ssh_client_channel:cache_insert(Cache, Channel, infinity), + gen_tcp:controlling_process(Sock, Pid), + inet:setopts(Sock, [{active,once}]), + {channel_open_confirmation_msg(RemoteId, ChId, WinSz, PktSz), +@@ -975,26 +975,26 @@ handle_msg(#ssh_msg_channel_open{channel_type = "forwarded-tcpip", + {[{connection_reply, ReplyMsg}], C#connection{channel_id_seed = NextChId}}; + + handle_msg(#ssh_msg_channel_open{channel_type = "direct-tcpip", +- sender_channel = RemoteId, ++ sender_channel = RemoteId, + initial_window_size = WindowSize, + maximum_packet_size = PacketSize, + data = <> +- }, +- #connection{channel_cache = Cache, ++ } = Msg, ++ #connection{channel_cache = Cache, + channel_id_seed = ChId, + suggest_window_size = WinSz, + suggest_packet_size = PktSz, + options = Options, + connection_supervisor = ConnectionSup + } = C, +- server, _SSH) -> +- {ReplyMsg, NextChId} = ++ server, SSH) -> ++ Result = + case ?GET_OPT(tcpip_tunnel_in, Options) of + %% May add more to the option, like allowed ip/port pairs to connect to + false -> +- {channel_open_failure_msg(RemoteId, ++ {channel_open_failure_msg(RemoteId, + ?SSH_OPEN_CONNECT_FAILED, + "Forwarding disabled", "en"), + ChId}; +@@ -1003,36 +1003,54 @@ handle_msg(#ssh_msg_channel_open{channel_type = "direct-tcpip", + case gen_tcp:connect(binary_to_list(HostToConnect), PortToConnect, + [{active,false}, binary]) of + {ok,Sock} -> +- {ok,Pid} = ssh_connection_sup:start_channel(server, ConnectionSup, self(), +- ssh_tcpip_forward_srv, ChId, +- [Sock], undefined, Options), +- ssh_client_channel:cache_update(Cache, +- #channel{type = "direct-tcpip", +- sys = "none", +- local_id = ChId, +- remote_id = RemoteId, +- user = Pid, +- recv_window_size = WinSz, +- recv_packet_size = PktSz, +- send_window_size = WindowSize, +- send_packet_size = PacketSize, +- send_buf = queue:new() +- }), +- gen_tcp:controlling_process(Sock, Pid), +- inet:setopts(Sock, [{active,once}]), +- +- {channel_open_confirmation_msg(RemoteId, ChId, WinSz, PktSz), +- ChId + 1}; ++ Limit = ?GET_OPT(max_channels, Options), ++ Channel = #channel{type = "direct-tcpip", ++ sys = "none", ++ local_id = ChId, ++ remote_id = RemoteId, ++ recv_window_size = WinSz, ++ recv_packet_size = PktSz, ++ send_window_size = WindowSize, ++ send_packet_size = PacketSize, ++ send_buf = queue:new() ++ }, ++ case ssh_client_channel:cache_insert(Cache, Channel, Limit) of ++ ok -> ++ {ok,Pid} = ssh_connection_sup:start_channel(server, ConnectionSup, self(), ++ ssh_tcpip_forward_srv, ChId, ++ [Sock], undefined), ++ ssh_client_channel:cache_update(Cache, Channel#channel{user = Pid}), ++ gen_tcp:controlling_process(Sock, Pid), ++ inet:setopts(Sock, [{active,once}]), ++ ++ {channel_open_confirmation_msg(RemoteId, ChId, WinSz, PktSz), ++ ChId + 1}; ++ {error, max_num_channels_exceeded} -> ++ gen_tcp:close(Sock), ++ MsgFun = fun(M, L) -> ++ io_lib:format("Connection terminated. Message: ~w" ++ " reached a limit of: ~p", [M, L], ++ [{chars_limit, ssh_lib:max_log_len(SSH)}]) ++ end, ++ ?LOG_DEBUG(MsgFun, [Msg, Limit]), ++ {send_disconnect, {?SSH_DISCONNECT_BY_APPLICATION, "Connection terminated. " ++ "Channel limit reached."}} ++ end; + + {error,Error} -> +- {channel_open_failure_msg(RemoteId, ++ {channel_open_failure_msg(RemoteId, + ?SSH_OPEN_CONNECT_FAILED, + io_lib:format("Forwarded connection refused: ~p",[Error]), + "en"), + ChId} + end + end, +- {[{connection_reply, ReplyMsg}], C#connection{channel_id_seed = NextChId}}; ++ case Result of ++ {send_disconnect, Reason} -> ++ {send_disconnect, Reason, handle_stop(C)}; ++ {ReplyMsg, NextChId} -> ++ {[{connection_reply, ReplyMsg}], C#connection{channel_id_seed = NextChId}} ++ end; + + handle_msg(#ssh_msg_channel_open{channel_type = "session", + sender_channel = RemoteId}, +@@ -1440,15 +1458,15 @@ encode_ip(Addr) when is_list(Addr) -> + + %%%---------------------------------------------------------------- + %%% Create the channel data when an ssh_msg_open_channel message +-%%% of "session" typ is handled ++%%% of "session" type is handled + %%% + setup_session(#connection{channel_cache = Cache, + channel_id_seed = NewChannelID, + suggest_window_size = WinSz, + suggest_packet_size = PktSz +- } = C, +- RemoteId, Type, WindowSize, PacketSize) when is_integer(WinSz), +- is_integer(PktSz) -> ++ } = C, ++ RemoteId, Type, WindowSize, PacketSize, ChannelLimit) when is_integer(WinSz), ++ is_integer(PktSz) -> + NextChannelID = NewChannelID + 1, + Channel = + #channel{type = Type, +@@ -1461,7 +1479,7 @@ setup_session(#connection{channel_cache = Cache, + send_buf = queue:new(), + remote_id = RemoteId + }, +- ssh_client_channel:cache_update(Cache, Channel), ++ ok = ssh_client_channel:cache_insert(Cache, Channel, ChannelLimit), + OpenConfMsg = channel_open_confirmation_msg(RemoteId, NewChannelID, + WinSz, + PktSz), +@@ -1472,15 +1490,15 @@ setup_session(#connection{channel_cache = Cache, + %%%---------------------------------------------------------------- + %%% Start a cli or subsystem + %%% +-start_cli(#connection{options = Options, +- cli_spec = CliSpec, ++start_cli(#connection{cli_spec = CliSpec, + exec = Exec, + connection_supervisor = ConnectionSup}, ChannelId) -> + case CliSpec of + no_cli -> + {error, cli_disabled}; + {CbModule, Args} -> +- ssh_connection_sup:start_channel(server, ConnectionSup, self(), CbModule, ChannelId, Args, Exec, Options) ++ ssh_connection_sup:start_channel(server, ConnectionSup, self(), ++ CbModule, ChannelId, Args, Exec) + end. + + +@@ -1490,7 +1508,8 @@ start_subsystem(BinName, #connection{options = Options, + Name = binary_to_list(BinName), + case check_subsystem(Name, Options) of + {Callback, Opts} when is_atom(Callback), Callback =/= none -> +- ssh_connection_sup:start_channel(server, ConnectionSup, self(), Callback, ChannelId, Opts, undefined, Options); ++ ssh_connection_sup:start_channel(server, ConnectionSup, self(), ++ Callback, ChannelId, Opts, undefined); + {none, _} -> + {error, bad_subsystem}; + {_, _} -> +--- a/lib/ssh/src/ssh_connection_handler.erl ++++ b/lib/ssh/src/ssh_connection_handler.erl +@@ -191,16 +191,16 @@ open_channel(ConnectionHandler, + Timeout}). + + %%-------------------------------------------------------------------- +-%%% Start a channel handling process in the superviser tree ++%%% Start a channel handling process in the supervisor tree + -spec start_channel(connection_ref(), atom(), channel_id(), list(), term()) -> + {ok, pid()} | {error, term()}. + + %% . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . + start_channel(ConnectionHandler, CallbackModule, ChannelId, Args, Exec) -> +- {ok, {ConnectionSup,Role,Opts}} = call(ConnectionHandler, get_misc), ++ {ok, {ConnectionSup,Role}} = call(ConnectionHandler, get_misc), + ssh_connection_sup:start_channel(Role, ConnectionSup, + ConnectionHandler, CallbackModule, ChannelId, +- Args, Exec, Opts). ++ Args, Exec). + + %%-------------------------------------------------------------------- + handle_direct_tcpip(ConnectionHandler, ListenHost, ListenPort, ConnectToHost, ConnectToPort, Timeout) -> +@@ -764,6 +764,13 @@ handle_event(internal, {conn_msg,Msg}, StateName, #data{connection_state = Conne + end, + {stop_and_reply, {shutdown,normal}, Repls, D}; + ++ {send_disconnect, {Code, Description}, RepliesConn} -> ++ {Replies, D1} = send_replies(RepliesConn, D0), ++ D = send_msg(#ssh_msg_disconnect{code = Code, ++ description = Description}, ++ D1), ++ {stop_and_reply, {shutdown, Description}, Replies, D}; ++ + {Replies, Connection} when is_list(Replies) -> + {Repls, D} = + case StateName of +@@ -1036,37 +1043,48 @@ handle_event({call,From}, {eof, ChannelId}, StateName, D0) + handle_event({call,From}, get_misc, StateName, + #data{connection_state = #connection{options = Opts}} = D) when ?CONNECTED(StateName) -> + ConnectionSup = ?GET_INTERNAL_OPT(connection_sup, Opts), +- Reply = {ok, {ConnectionSup, ?role(StateName), Opts}}, ++ Reply = {ok, {ConnectionSup, ?role(StateName)}}, + {keep_state, D, [{reply,From,Reply}]}; + + handle_event({call,From}, +- {open, ChannelPid, Type, InitialWindowSize, MaxPacketSize, Data, Timeout}, +- StateName, +- D0 = #data{connection_state = C}) when ?CONNECTED(StateName) -> +- erlang:monitor(process, ChannelPid), +- {ChannelId, D1} = new_channel_id(D0), ++ {open, ChannelPid, Type, InitialWindowSize, MaxPacketSize, Data, Timeout}, ++ StateName, ++ D0 = #data{connection_state = C = #connection{channel_id_seed = ChannelId, ++ suggest_window_size = SuggestWindowSize, ++ suggest_packet_size = SuggestPacketSize}}) ++ when ?CONNECTED(StateName) -> + WinSz = case InitialWindowSize of +- undefined -> C#connection.suggest_window_size; ++ undefined -> SuggestWindowSize; + _ -> InitialWindowSize + end, + PktSz = case MaxPacketSize of +- undefined -> C#connection.suggest_packet_size; ++ undefined -> SuggestPacketSize; + _ -> MaxPacketSize + end, +- D2 = send_msg(ssh_connection:channel_open_msg(Type, ChannelId, WinSz, PktSz, Data), +- D1), +- ssh_client_channel:cache_update(cache(D2), +- #channel{type = Type, +- sys = "none", +- user = ChannelPid, +- local_id = ChannelId, +- recv_window_size = WinSz, +- recv_packet_size = PktSz, +- send_buf = queue:new() +- }), +- D = add_request(true, ChannelId, From, D2), +- start_channel_request_timer(ChannelId, From, Timeout), +- {keep_state, D, cond_set_idle_timer(D)}; ++ Limit = case ?role(StateName) of ++ server -> ?GET_OPT(max_channels, C#connection.options); ++ client -> infinity ++ end, ++ Channel = #channel{type = Type, ++ sys = "none", ++ user = ChannelPid, ++ local_id = ChannelId, ++ recv_window_size = WinSz, ++ recv_packet_size = PktSz, ++ send_buf = queue:new() ++ }, ++ case ssh_client_channel:cache_insert(cache(D0), Channel, Limit) of ++ ok -> ++ erlang:monitor(process, ChannelPid), ++ D1 = D0#data{connection_state = C#connection{channel_id_seed = ChannelId + 1}}, ++ D2 = send_msg(ssh_connection:channel_open_msg(Type, ChannelId, WinSz,PktSz, Data), ++ D1), ++ D = add_request(true, ChannelId, From, D2), ++ start_channel_request_timer(ChannelId, From, Timeout), ++ {keep_state, D, cond_set_idle_timer(D)}; ++ Other -> ++ {keep_state, D0, [{reply,From,Other}]} ++ end; + + handle_event({call,From}, {send_window, ChannelId}, StateName, D) + when ?CONNECTED(StateName) -> +@@ -1362,16 +1380,39 @@ handle_event(info, check_cache, _, D) -> + {keep_state, D, cond_set_idle_timer(D)}; + + handle_event(info, {fwd_connect_received, Sock, ChId, ChanCB}, StateName, #data{connection_state = Connection}) -> +- #connection{options = Options, +- channel_cache = Cache, ++ #connection{channel_cache = Cache, + connection_supervisor = ConnectionSup} = Connection, + Channel = ssh_client_channel:cache_lookup(Cache, ChId), +- {ok,Pid} = ssh_connection_sup:start_channel(?role(StateName), ConnectionSup, self(), ChanCB, ChId, [Sock], undefined, Options), ++ {ok,Pid} = ssh_connection_sup:start_channel(?role(StateName), ConnectionSup, self(), ChanCB, ChId, [Sock], undefined), + ssh_client_channel:cache_update(Cache, Channel#channel{user=Pid}), + gen_tcp:controlling_process(Sock, Pid), + inet:setopts(Sock, [{active,once}]), + keep_state_and_data; + ++handle_event(info, {fwd_connect_failed, {error, max_num_channels_exceeded}}, StateName, ++ #data{connection_state = #connection{options = Opts}} = D0) -> ++ %% Keep same behavior as before, if channel limit is reached for port forwarding channels ++ %% we close the connection. Previously this was a crash in {fwd_connect_received, ...} on ++ %% {ok, Pid} = ssh_connection_sup:start_channel(...). ++ MsgFun = ++ fun(debug) -> ++ Limit = ?GET_OPT(max_channels, Opts), ++ io_lib:format("Connection terminated. Port forward request reached a " ++ "channel limit of: ~p", ++ [Limit]); ++ (_) -> ++ "Connection terminated. Channel limit reached." ++ end, ++ {Shutdown, D} = ++ ?send_disconnect(?SSH_DISCONNECT_BY_APPLICATION, ++ "Connection terminated. Channel limit reached.", ++ ?SELECT_MSG(MsgFun), ++ StateName, D0), ++ {stop, Shutdown, D}; ++handle_event(info, {fwd_connect_failed, _Other}, _StateName, _D) -> ++ %% Ignore other reasons ++ keep_state_and_data; ++ + handle_event({call,From}, + {handle_direct_tcpip, ListenHost, ListenPort, ConnectToHost, ConnectToPort, _Timeout}, + _StateName, +@@ -1794,12 +1835,6 @@ add_request(Fun, ChannelId, From, #data{connection_state = + Requests = [{ChannelId, From, Fun} | Requests0], + State#data{connection_state = Connection#connection{requests = Requests}}. + +-new_channel_id(#data{connection_state = #connection{channel_id_seed = Id} = +- Connection} +- = State) -> +- {Id, State#data{connection_state = +- Connection#connection{channel_id_seed = Id + 1}}}. +- + + %%%---------------------------------------------------------------- + start_rekeying(Role, D0) -> +--- a/lib/ssh/src/ssh_connection_sup.erl ++++ b/lib/ssh/src/ssh_connection_sup.erl +@@ -30,7 +30,7 @@ + -include("ssh.hrl"). + + -export([start_link/4, +- start_channel/8, ++ start_channel/7, + tcpip_fwd_supervisor/1 + ]). + +@@ -48,9 +48,9 @@ start_link(Role, Id, Socket, Options) -> + Other + end. + +-start_channel(Role, SupPid, ConnRef, Callback, Id, Args, Exec, Opts) -> ++start_channel(Role, SupPid, ConnRef, Callback, Id, Args, Exec) -> + ChannelSup = channel_supervisor(SupPid), +- ssh_channel_sup:start_child(Role, ChannelSup, ConnRef, Callback, Id, Args, Exec, Opts). ++ ssh_channel_sup:start_child(Role, ChannelSup, ConnRef, Callback, Id, Args, Exec). + + tcpip_fwd_supervisor(ConnectionSup) -> + find_child(tcpip_forward_acceptor_sup, ConnectionSup). +--- a/lib/ssh/src/ssh_tcpip_forward_acceptor.erl ++++ b/lib/ssh/src/ssh_tcpip_forward_acceptor.erl +@@ -88,8 +88,9 @@ acceptor_loop(LSock, ListenAddrStr, ListenPort, ConnectToAddr, ChanType, ChanCB, + {ok,ChId} -> + gen_tcp:controlling_process(Sock, ConnPid), + ConnPid ! {fwd_connect_received, Sock, ChId, ChanCB}; +- _ -> +- gen_tcp:close(Sock) ++ Other -> ++ gen_tcp:close(Sock), ++ ConnPid ! {fwd_connect_failed, Other} + end, + acceptor_loop(LSock, ListenAddrStr, ListenPort, ConnectToAddr, ChanType, ChanCB, ConnPid); + +--- a/lib/ssh/test/ssh_connection_SUITE.erl ++++ b/lib/ssh/test/ssh_connection_SUITE.erl +@@ -72,6 +72,8 @@ + kex_error/1, + interrupted_send/1, + max_channels_option/1, ++ max_channels_to_server/1, ++ max_channels_from_server/1, + no_sensitive_leak/1, + ptty_alloc/1, + ptty_alloc_default/1, +@@ -182,6 +184,8 @@ all() -> + no_sensitive_leak, + start_subsystem_on_closed_channel, + max_channels_option, ++ max_channels_to_server, ++ max_channels_from_server, + handler_down_before_open + ]. + groups() -> +@@ -359,6 +363,12 @@ print_interesting_events([_|Tail], Cnt) -> + verify_events(_TestCase, 0) -> ok; + verify_events(no_sensitive_leak, 1) -> ok; + verify_events(max_channels_option, 3) -> ok; ++verify_events(max_channels_to_server, 1) -> ok; ++verify_events(max_channels_to_server, 2) -> ok; ++verify_events(max_channels_to_server, 3) -> ok; ++verify_events(max_channels_from_server, 1) -> ok; ++verify_events(max_channels_from_server, 2) -> ok; ++verify_events(max_channels_from_server, 3) -> ok; + verify_events(_TestCase, EventNumber) when EventNumber > 0-> + {fail, lists:flatten( + io_lib:format("unexpected event cnt: ~s", +@@ -1888,19 +1898,22 @@ max_channels_option(Config) when is_list(Config) -> + {user_interaction, true}, + {user_dir, UserDir}]), + +- %% Allocate a number of ChannelId:s to play with. (This operation is not +- %% counted by the max_channel option). ++ %% Allocate a number of ChannelId:s to play with. + {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), + {ok, ChannelId1} = ssh_connection:session_channel(ConnectionRef, infinity), + {ok, ChannelId2} = ssh_connection:session_channel(ConnectionRef, infinity), +- {ok, ChannelId3} = ssh_connection:session_channel(ConnectionRef, infinity), +- {ok, ChannelId4} = ssh_connection:session_channel(ConnectionRef, infinity), +- {ok, ChannelId5} = ssh_connection:session_channel(ConnectionRef, infinity), +- {ok, ChannelId6} = ssh_connection:session_channel(ConnectionRef, infinity), +- {ok, _ChannelId7} = ssh_connection:session_channel(ConnectionRef, infinity), +- +- %% Now start to open the channels (this is counted my max_channels) to check that +- %% it gives a failure at right place ++ {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} = ++ ssh_connection:session_channel(ConnectionRef, infinity), ++ {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} = ++ ssh_connection:session_channel(ConnectionRef, infinity), ++ {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} = ++ ssh_connection:session_channel(ConnectionRef, infinity), ++ {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} = ++ ssh_connection:session_channel(ConnectionRef, infinity), ++ {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} = ++ ssh_connection:session_channel(ConnectionRef, infinity), ++ ++ %% Now start subsystems in open channels. + + %%%---- Channel 1(3): shell + ok = ssh_connection:shell(ConnectionRef,ChannelId0), +@@ -1922,16 +1935,29 @@ max_channels_option(Config) when is_list(Config) -> + after 5000 -> + ct:fail("Exec #1 Timeout") + end, ++ %%%---- wait for exec to terminate ++ receive ++ {ssh_cm,ConnectionRef,{closed,ChannelId2}} -> ok ++ after 5000 -> ++ ct:log("Timeout waiting for '{ssh_cm,~p,{closed,~p}}'~n" ++ "Message queue:~n~p", ++ [ConnectionRef,ChannelId2,erlang:process_info(self(),messages)]), ++ ct:fail("exit Timeout",[]) ++ end, ++ ++ %% Now ChannelId2 should be closed now, we can open ChannelId3 ++ {ok, ChannelId3} = ssh_connection:session_channel(ConnectionRef, infinity), + + %%%---- Channel 3(3): subsystem "echo_n" (Note that ChannelId2 should be closed now) + ?wait_match(success, ssh_connection:subsystem(ConnectionRef, ChannelId3, "echo_n", infinity)), + +- %%%---- Channel 4(3) !: exec This should fail +- failure = ssh_connection:exec(ConnectionRef, ChannelId4, "testing2.\n", infinity), ++ %%%---- Channel 4(3) !: creating channel should fail ++ {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} = ++ ssh_connection:session_channel(ConnectionRef, infinity), + + %%%---- close the shell (Frees one channel) + ok = ssh_connection:send(ConnectionRef, ChannelId0, "exit().\n", 5000), +- ++ + %%%---- wait for the subsystem to terminate + receive + {ssh_cm,ConnectionRef,{closed,ChannelId0}} -> ok +@@ -1942,15 +1968,114 @@ max_channels_option(Config) when is_list(Config) -> + ct:fail("exit Timeout",[]) + end, + ++ %%%---- Channel 3(3) !: exec This should succeed ++ {ok, ChannelId4} = ssh_connection:session_channel(ConnectionRef, infinity), ++ success = ssh_connection:exec(ConnectionRef, ChannelId4, "testing2.\n", infinity), ++ %%%---- wait for exec to terminate ++ receive ++ {ssh_cm,ConnectionRef,{closed,ChannelId4}} -> ok ++ after 5000 -> ++ ct:log("Timeout waiting for '{ssh_cm,~p,{closed,~p}}'~n" ++ "Message queue:~n~p", ++ [ConnectionRef,ChannelId4,erlang:process_info(self(),messages)]), ++ ct:fail("exit Timeout",[]) ++ end, ++ + %%---- Try that we can open one channel instead of the closed one ++ {ok, ChannelId5} = ssh_connection:session_channel(ConnectionRef, infinity), + ?wait_match(success, ssh_connection:subsystem(ConnectionRef, ChannelId5, "echo_n", infinity)), + + %%---- But not a fourth one... +- failure = ssh_connection:subsystem(ConnectionRef, ChannelId6, "echo_n", infinity), ++ {open_error, ?SSH_OPEN_CONNECT_FAILED, "Connection refused", <<"en">>} = ++ ssh_connection:session_channel(ConnectionRef, infinity), + + ssh:close(ConnectionRef), + ssh:stop_daemon(Pid). + ++%%-------------------------------------------------------------------- ++max_channels_to_server(Config) when is_list(Config) -> ++ max_channels_forwarding_helper(Config, true). ++ ++%%-------------------------------------------------------------------- ++max_channels_from_server(Config) when is_list(Config) -> ++ max_channels_forwarding_helper(Config, false). ++ ++max_channels_forwarding_helper(Config, ToServer) -> ++ PrivDir = proplists:get_value(priv_dir, Config), ++ UserDir = filename:join(PrivDir, nopubkey), ++ file:make_dir(UserDir), ++ SysDir = proplists:get_value(data_dir, Config), ++ ++ {ok, TargetSock} = gen_tcp:listen(0, [{active, false}, binary, {reuseaddr, true}]), ++ {ok, {TargetHost, TargetPort}} = inet:sockname(TargetSock), ++ ++ Options = ++ case ToServer of ++ true -> ++ [{tcpip_tunnel_in, true}]; ++ false -> ++ [{tcpip_tunnel_out, true}] ++ end, ++ ++ {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, ++ {user_dir, UserDir}, ++ {password, "morot"}, ++ {max_channels, 2}] ++ ++ Options), ++ ++ Ref = make_ref(), ++ Parent = self(), ++ ConnectionRef = ssh_test_lib:connect(Host, Port, ++ [{silently_accept_hosts, true}, ++ {user, "foo"}, ++ {password, "morot"}, ++ {user_interaction, false}, ++ {user_dir, UserDir}, ++ {disconnectfun, fun(Reason) -> Parent ! {Ref, Reason} end}]), ++ ++ {ok, ListenPort} = ++ case ToServer of ++ true -> ++ ssh:tcpip_tunnel_to_server(ConnectionRef, ++ {127,0,0,1}, ++ 0, ++ TargetHost, ++ TargetPort, ++ timer:seconds(5)); ++ false -> ++ ssh:tcpip_tunnel_from_server(ConnectionRef, ++ {127,0,0,1}, ++ 0, ++ TargetHost, ++ TargetPort, ++ timer:seconds(5)) ++ end, ++ ++ {ok, Sock1} = gen_tcp:connect("127.0.0.1", ListenPort, [{active, false}]), ++ {ok, Sock2} = gen_tcp:connect("127.0.0.1", ListenPort, [{active, false}]), ++ {ok, _} = gen_tcp:accept(TargetSock), ++ {ok, _} = gen_tcp:accept(TargetSock), ++ ++ %% Sockets are connected ++ {error, timeout} = gen_tcp:recv(Sock1, 0, 0), ++ {error, timeout} = gen_tcp:recv(Sock2, 0, 0), ++ ++ %% Upon hitting the channel limit, server will close all sockets ++ {ok, Sock3} = gen_tcp:connect("127.0.0.1", ListenPort, [{active, false}]), ++ receive ++ {Ref, "Received disconnect: Connection terminated. Channel limit reached."} -> ++ ok ++ after 2000 -> ++ ct:fail("Connection should be closed!") ++ end, ++ {error, closed} = gen_tcp:recv(Sock1, 0, timer:seconds(5)), ++ {error, closed} = gen_tcp:recv(Sock2, 0, timer:seconds(5)), ++ {error, closed} = gen_tcp:recv(Sock3, 0, timer:seconds(5)), ++ ++ gen_tcp:close(TargetSock), ++ ssh:stop_daemon(Pid). ++ ++%%-------------------------------------------------------------------- + handler_down_before_open(Config) -> + %% Start echo subsystem with a delay in init() - until a signal is received + %% One client opens a channel on the connection +--- a/lib/ssh/test/ssh_options_SUITE.erl ++++ b/lib/ssh/test/ssh_options_SUITE.erl +@@ -28,6 +28,7 @@ + -include_lib("common_test/include/ct.hrl"). + -include_lib("kernel/include/file.hrl"). + -include("ssh_test_lib.hrl"). ++-include("ssh_connect.hrl"). + + %%% Test cases + -export([ +@@ -1197,7 +1198,7 @@ ssh_daemon_minimal_remote_max_packet_size_option(Config) -> + + %% Try the limits of the minimal_remote_max_packet_size: + {ok, _ChannelId} = ssh_connection:session_channel(Conn, 100, 14, infinity), +- {open_error,_,"Maximum packet size below 14 not supported",_} = ++ {open_error, ?SSH_OPEN_ADMINISTRATIVELY_PROHIBITED, "Maximum packet size below 14 not supported", <<"en">>} = + ssh_connection:session_channel(Conn, 100, 13, infinity), + + ssh:close(Conn), diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-2.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-2.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-2.patch 2026-09-30 10:13:04.000000000 +0000 @@ -0,0 +1,157 @@ +From: Michał Wąsowski +Date: Mon, 31 Aug 2026 19:07:06 +0200 +Subject: ssh: Change max_sessions and max_channels defaults and update terminology + +Origin: upstream, https://github.com/erlang/otp/commit/e2a9be0a4b0035331ae6432b258d446dd5e7579a +Comment: Documentation-only hunks for lib/ssh/doc/guides/hardening.md and + lib/ssh/doc/guides/terminology.md dropped. +--- + lib/ssh/src/ssh.hrl | 29 ++++++++++++++++---- + lib/ssh/src/ssh_options.erl | 16 ++++++++--- + lib/ssh/test/ssh_options_SUITE.erl | 27 ++++++++++++++++-- + 3 files changed, 60 insertions(+), 12 deletions(-) + +--- a/lib/ssh/src/ssh.hrl ++++ b/lib/ssh/src/ssh.hrl +@@ -1102,14 +1102,30 @@ in the User's Guide chapter. + Notice that if `parallel_login` is `false`, only one client at a time can be + in the authentication phase. + +- By default, this option is not set. This means that the number is not limited. ++ By default 1024 sessions are accepted. The value `infinity` means there is no limit. ++ ++ > #### Info {: .info } ++ > ++ > This limits all TCP connections (both authenticated and unauthenticated) to ++ > the daemon, not SSH session channels. Compare with OpenSSH `MaxStartups` ++ > which limits only unauthenticated connections. See the ++ > [Terminology](terminology.md#connection-channel-and-session) guide for ++ > details on connection vs channel vs session. + + - **`max_channels`{: #hardening_daemon_options-max_channels }** - The maximum + number of channels that are accepted for each connection to this daemon. This + includes channels that are automatically opened when a port forwarding request + is serviced by the server. + +- By default, this option is not set. This means that the number is not limited. ++ By default 256 channels are accepted. The value `infinity` means there is no limit. ++ ++ > #### Info {: .info } ++ > ++ > This limits all channel types (session, direct-tcpip, forwarded-tcpip) per ++ > connection. Compare with OpenSSH `MaxSessions`, which limits only session ++ > channels. See the ++ > [Terminology](terminology.md#connection-channel-and-session) guide for ++ > details on connection vs channel vs session. + + - **`parallel_login`{: #hardening_daemon_options-parallel_login }** - If set to + false (the default value), only one login is handled at a time. If set to +@@ -1123,8 +1139,9 @@ in the User's Guide chapter. + > #### Warning {: .warning } + > + > Do not enable `parallel_logins` without protecting the server by other +- > means, for example, by the `max_sessions` option or a firewall +- > configuration. If set to `true`, there is no protection against DOS attacks. ++ > means, for example, by adjusting `max_sessions` option to your needs or a ++ > firewall configuration. If set to `true` when `max_sessions` is set to ++ > `infinity` there is no protection against DOS attacks. + + - **`minimal_remote_max_packet_size`{: + #hardening_daemon_options-minimal_remote_max_packet_size }** - The least +@@ -1133,8 +1150,8 @@ in the User's Guide chapter. + """. + -doc(#{title => <<"Daemon Options">>}). + -type hardening_daemon_options() :: +- {max_sessions, pos_integer()} +- | {max_channels, pos_integer()} ++ {max_sessions, pos_integer() | infinity} ++ | {max_channels, pos_integer() | infinity} + | {parallel_login, boolean()} + | {minimal_remote_max_packet_size, pos_integer()}. + +--- a/lib/ssh/src/ssh_options.erl ++++ b/lib/ssh/src/ssh_options.erl +@@ -552,14 +552,14 @@ default(server) -> + }, + + max_sessions => +- #{default => infinity, +- chk => fun(V) -> check_pos_integer(V) end, ++ #{default => 1024, ++ chk => fun(V) -> check_max_sessions(V) end, + class => user_option + }, + + max_channels => +- #{default => infinity, +- chk => fun(V) -> check_pos_integer(V) end, ++ #{default => 256, ++ chk => fun(V) -> check_max_channels(V) end, + class => user_option + }, + +@@ -914,6 +914,14 @@ error_in_check(BadValue, Extra) -> error({check,{BadValue,Extra}}). + check_timeout(infinity) -> true; + check_timeout(I) -> check_pos_integer(I). + ++%%%---------------------------------------------------------------- ++check_max_sessions(infinity) -> true; ++check_max_sessions(I) -> check_pos_integer(I). ++ ++%%%---------------------------------------------------------------- ++check_max_channels(infinity) -> true; ++check_max_channels(I) -> check_pos_integer(I). ++ + %%%---------------------------------------------------------------- + check_pos_integer(I) -> is_integer(I) andalso I>0. + +--- a/lib/ssh/test/ssh_options_SUITE.erl ++++ b/lib/ssh/test/ssh_options_SUITE.erl +@@ -54,6 +54,8 @@ + max_sessions_ssh_connect_sequential/1, + max_sessions_drops_tcp_connects/1, + max_sessions_drops_tcp_connects/0, ++ max_sessions_option/1, ++ max_channels_option/1, + server_password_option/1, + server_userpassword_option/1, + server_pwdfun_option/1, +@@ -174,7 +176,9 @@ groups() -> + max_sessions_ssh_connect_sequential, + max_sessions_sftp_start_channel_parallel, + max_sessions_sftp_start_channel_sequential, +- max_sessions_drops_tcp_connects ++ max_sessions_drops_tcp_connects, ++ max_sessions_option, ++ max_channels_option + ]}, + {dir_options, [], [user_dir_option, + user_dir_fun_option, +@@ -1647,7 +1651,26 @@ try_ssh_connect(_N, _NegTimeOut, _F) -> + oks(L) -> lists:filter(fun({ok,_}) -> true; + (_) -> false + end, L). +- ++ ++%%-------------------------------------------------------------------- ++max_sessions_option(_Config) -> ++ #{max_sessions := 1024} = ssh_options:handle_options(server, []), ++ #{max_sessions := 1024} = ssh_options:handle_options(server, [{max_sessions, 1024}]), ++ #{max_sessions := 1} = ssh_options:handle_options(server, [{max_sessions, 1}]), ++ #{max_sessions := infinity} = ssh_options:handle_options(server, [{max_sessions, infinity}]), ++ {error, {eoptions, _}} = ssh_options:handle_options(server, [{max_sessions, 0}]), ++ {error, {eoptions, _}} = ssh_options:handle_options(server, [{max_sessions, -1}]), ++ {error, {eoptions, _}} = ssh_options:handle_options(server, [{max_sessions, foo}]). ++ ++max_channels_option(_Config) -> ++ #{max_channels := 256} = ssh_options:handle_options(server, []), ++ #{max_channels := 1024} = ssh_options:handle_options(server, [{max_channels, 1024}]), ++ #{max_channels := 1} = ssh_options:handle_options(server, [{max_channels, 1}]), ++ #{max_channels := infinity} = ssh_options:handle_options(server, [{max_channels, infinity}]), ++ {error, {eoptions, _}} = ssh_options:handle_options(server, [{max_channels, 0}]), ++ {error, {eoptions, _}} = ssh_options:handle_options(server, [{max_channels, -1}]), ++ {error, {eoptions, _}} = ssh_options:handle_options(server, [{max_channels, foo}]). ++ + %%-------------------------------------------------------------------- + save_accepted_host_option(Config) -> + UserDir = proplists:get_value(user_dir, Config), diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-3.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-3.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-3.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-3.patch 2026-10-19 04:30:10.000000000 +0000 @@ -0,0 +1,190 @@ +From: Michał Wąsowski +Date: Mon, 24 Aug 2026 11:04:42 +0200 +Subject: ssh: Prevent duplicate subsystem spawns on same channel + +Origin: upstream, https://github.com/erlang/otp/commit/255b98b0380b4f5254e0c8ffdde994458c4c24d1 +--- + lib/ssh/src/ssh_connection.erl | 36 ++++++++----- + lib/ssh/test/ssh_connection_SUITE.erl | 95 +++++++++++++++++++++++++++++++---- + 2 files changed, 109 insertions(+), 22 deletions(-) + +--- a/lib/ssh/src/ssh_connection.erl ++++ b/lib/ssh/src/ssh_connection.erl +@@ -1138,21 +1138,31 @@ handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, + binary_to_list(SigName)}); + + handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, +- request_type = "subsystem", +- want_reply = WantReply, +- data = Data}, +- #connection{channel_cache = Cache} = Connection, server, _SSH) -> ++ request_type = "subsystem", ++ want_reply = WantReply, ++ data = Data}, ++ #connection{channel_cache = Cache} = Connection, server, _SSH) -> + <> = Data, +- #channel{remote_id=RemoteId} = Channel = +- ssh_client_channel:cache_lookup(Cache, ChannelId), ++ #channel{remote_id=RemoteId,user=User} = Channel = ++ ssh_client_channel:cache_lookup(Cache, ChannelId), + Reply = +- case start_subsystem(SsName, Connection, Channel, +- {subsystem, ChannelId, WantReply, binary_to_list(SsName)}) of +- {ok, Pid} -> +- erlang:monitor(process, Pid), +- ssh_client_channel:cache_update(Cache, Channel#channel{user=Pid}), +- channel_success_msg(RemoteId); +- {error,_Error} -> ++ case User of ++ undefined -> ++ case start_subsystem(SsName, Connection, Channel, ++ {subsystem, ChannelId, WantReply, binary_to_list(SsName)}) of ++ {ok, Pid} -> ++ erlang:monitor(process, Pid), ++ ssh_client_channel:cache_update(Cache, Channel#channel{user=Pid}), ++ channel_success_msg(RemoteId); ++ {error,_Error} -> ++ channel_failure_msg(RemoteId) ++ end; ++ _ -> ++ %% If shell, exec or any subsystem is active on the channel, starting another ++ %% subsystem must fail. See RFC 4254 6.5 ++ %% In our case 'env' (RFC 4254 6.6) and 'pty-req' (RFC 4254 6.2) ++ %% (even if any of them failed) also start cli handler, so if any of them are ++ %% started, trying to start any subsystem will also fail here. + channel_failure_msg(RemoteId) + end, + {[{connection_reply,Reply}], Connection}; +--- a/lib/ssh/test/ssh_connection_SUITE.erl ++++ b/lib/ssh/test/ssh_connection_SUITE.erl +@@ -108,6 +108,11 @@ + start_shell_sock_daemon_exec_multi/1, + start_shell_sock_exec_fun/1, + start_subsystem_on_closed_channel/1, ++ start_subsystem_on_channel_with_subsystem/1, ++ start_subsystem_on_channel_with_pty_req/1, ++ start_subsystem_on_channel_with_shell/1, ++ start_subsystem_on_channel_with_exec/1, ++ start_subsystem_on_channel_with_env/1, + stop_listener/1, + trap_exit_connect/1, + trap_exit_daemon/1, +@@ -183,6 +188,11 @@ all() -> + stop_listener, + no_sensitive_leak, + start_subsystem_on_closed_channel, ++ start_subsystem_on_channel_with_subsystem, ++ start_subsystem_on_channel_with_pty_req, ++ start_subsystem_on_channel_with_shell, ++ start_subsystem_on_channel_with_exec, ++ start_subsystem_on_channel_with_env, + max_channels_option, + max_channels_to_server, + max_channels_from_server, +@@ -1842,23 +1852,29 @@ no_sensitive_leak(Config) -> + {_, _, 0} -> ok; + {_, _, Nt0} -> ct:fail("Leak in ~p cases!", [Nt0]) + end. +- +-start_subsystem_on_closed_channel(Config) -> ++ ++start_server_and_connect_client(Config) -> ++ start_server_and_connect_client(Config, []). ++start_server_and_connect_client(Config, DaemonOpts) -> + PrivDir = proplists:get_value(priv_dir, Config), + UserDir = filename:join(PrivDir, nopubkey), % to make sure we don't use public-key-auth + file:make_dir(UserDir), + SysDir = proplists:get_value(data_dir, Config), + {Pid, Host, Port} = ssh_test_lib:daemon([{system_dir, SysDir}, +- {user_dir, UserDir}, +- {password, "morot"}, +- {subsystems, [{"echo_n", {ssh_echo_server, [4000000]}}]}]), ++ {user_dir, UserDir}, ++ {password, "morot"}, ++ {subsystems, [{"echo_n", {ssh_echo_server, [4000000]}}]}] ++ DaemonOpts), + + ConnectionRef = ssh_test_lib:connect(Host, Port, [{silently_accept_hosts, true}, +- {user, "foo"}, +- {password, "morot"}, +- {user_interaction, false}, +- {user_dir, UserDir}]), ++ {user, "foo"}, ++ {password, "morot"}, ++ {user_interaction, false}, ++ {user_dir, UserDir}]), + ++ {ConnectionRef, Pid}. ++ ++start_subsystem_on_closed_channel(Config) -> ++ {ConnectionRef, Pid} = start_server_and_connect_client(Config), + + {ok, ChannelId1} = ssh_connection:session_channel(ConnectionRef, infinity), + ok = ssh_connection:close(ConnectionRef, ChannelId1), +@@ -1879,6 +1895,67 @@ start_subsystem_on_closed_channel(Config) -> + ssh:close(ConnectionRef), + ssh:stop_daemon(Pid). + ++start_server_connect_client_and_get_channel(Config) -> ++ start_server_connect_client_and_get_channel(Config, []). ++start_server_connect_client_and_get_channel(Config, DaemonOpts) -> ++ {ConnectionRef, Pid} = start_server_and_connect_client(Config, DaemonOpts), ++ {ok, ChannelId} = ssh_connection:session_channel(ConnectionRef, infinity), ++ {ConnectionRef, ChannelId, Pid}. ++ ++start_subsystem_on_channel_with_subsystem(Config) -> ++ {ConnectionRef, ChannelId, Pid} = start_server_connect_client_and_get_channel(Config), ++ success = ssh_connection:subsystem(ConnectionRef, ChannelId, "echo_n", 5000), ++ check_subsystem_start_failure(ConnectionRef, ChannelId, Pid). ++ ++start_subsystem_on_channel_with_pty_req(Config) -> ++ DaemonOpts = [{shell, fun(_U, _P) -> spawn(fun() -> timer:sleep(infinity) end) end}], ++ {ConnectionRef, ChannelId, Pid} = start_server_connect_client_and_get_channel(Config, DaemonOpts), ++ success = ssh_connection:ptty_alloc(ConnectionRef, ChannelId, []), ++ check_subsystem_start_failure(ConnectionRef, ChannelId, Pid). ++ ++start_subsystem_on_channel_with_shell(Config) -> ++ DaemonOpts = [{shell, fun(_U, _P) -> spawn(fun() -> timer:sleep(infinity) end) end}], ++ {ConnectionRef, ChannelId, Pid} = start_server_connect_client_and_get_channel(Config, DaemonOpts), ++ ok = ssh_connection:shell(ConnectionRef, ChannelId), ++ check_subsystem_start_failure(ConnectionRef, ChannelId, Pid). ++ ++start_subsystem_on_channel_with_exec(Config) -> ++ Parent = self(), ++ Ref = make_ref(), ++ Fun = ++ fun(Cmd) -> ++ spawn(fun() -> ++ %% Keep subsystem alive until parent says it's ok ++ %% otherwise the channel could be closed before we tried to start "echo_n" ++ Parent ! {self(), Ref}, ++ io:format("echo ~s\n", [Cmd]), ++ receive {Parent, Ref} -> ok end ++ end) ++ end, ++ DaemonOpts = [{exec, Fun}], ++ {ConnectionRef, ChannelId, Pid} = start_server_connect_client_and_get_channel(Config, DaemonOpts), ++ success = ssh_connection:exec(ConnectionRef, ChannelId, "testing1.\n", 5000), ++ receive ++ {ExecPid, Ref} -> ++ failure = ssh_connection:subsystem(ConnectionRef, ChannelId, "echo_n", 5000), ++ ExecPid ! {self(), Ref}, ++ ssh:close(ConnectionRef), ++ ssh:stop_daemon(Pid) ++ end. ++ ++start_subsystem_on_channel_with_env(Config) -> ++ DaemonOpts = [{shell, fun(_U, _P) -> spawn(fun() -> timer:sleep(infinity) end) end}], ++ {ConnectionRef, ChannelId, Pid} = start_server_connect_client_and_get_channel(Config, DaemonOpts), ++ %% setenv is not implemented, and will return a failure, but cli handler will be started anyway ++ failure = ssh_connection:setenv(ConnectionRef, ChannelId, "ENV_TEST", "VALUE", infinity), ++ check_subsystem_start_failure(ConnectionRef, ChannelId, Pid). ++ ++check_subsystem_start_failure(ConnectionRef, ChannelId, Pid) -> ++ failure = ssh_connection:subsystem(ConnectionRef, ChannelId, "echo_n", 5000), ++ ++ ssh:close(ConnectionRef), ++ ssh:stop_daemon(Pid). ++ + %%-------------------------------------------------------------------- + max_channels_option(Config) when is_list(Config) -> + PrivDir = proplists:get_value(priv_dir, Config), diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-4.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-4.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-4.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-68956-4.patch 2026-10-19 04:30:17.000000000 +0000 @@ -0,0 +1,166 @@ +From: Michał Wąsowski +Date: Fri, 18 Sep 2026 20:21:46 +0200 +Subject: ssh: Allow subsystem to start after env and pty requests + +Origin: upstream, https://github.com/erlang/otp/commit/1c8a6c17578da315fd5bb2c3c2f84fe33ec34da8 +--- + lib/ssh/src/ssh_client_channel.erl | 6 ++++ + lib/ssh/src/ssh_connect.hrl | 2 +- + lib/ssh/src/ssh_connection.erl | 52 +++++++++++++++++++++++++++++------ + lib/ssh/test/ssh_connection_SUITE.erl | 16 +++++++++++ + 4 files changed, 66 insertions(+), 10 deletions(-) + +--- a/lib/ssh/src/ssh_client_channel.erl ++++ b/lib/ssh/src/ssh_client_channel.erl +@@ -502,6 +502,12 @@ handle_info({ssh_cm, _, _} = Msg, #state{channel_cb = Module, + do_the_close(Msg, State#state.channel_id, State) + end; + ++handle_info({ssh_channel_handler_replaced, ConnectionManager}, ++ #state{cm = ConnectionManager} = State) -> ++ %% The channel handler process was replaced with another one. ++ %% Set close_sent = true, so that we don't close the channel itself ++ {stop, normal, State#state{close_sent = true}}; ++ + handle_info(Msg, #state{channel_cb = Module, + channel_state = ChannelState0} = State) -> + try Module:handle_msg(Msg, ChannelState0) +--- a/lib/ssh/src/ssh_connect.hrl ++++ b/lib/ssh/src/ssh_connect.hrl +@@ -240,7 +240,7 @@ + -record(channel, + { + type, %% "session" +- sys, %% "none", "shell", "exec" "subsystem" ++ sys, %% "none", "ssh", "shell", "exec", "subsystem", "env", "pty" + user, %% "user" process id (default to cm user) + flow_control, + +--- a/lib/ssh/src/ssh_connection.erl ++++ b/lib/ssh/src/ssh_connection.erl +@@ -1146,23 +1146,21 @@ handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, + #channel{remote_id=RemoteId,user=User} = Channel = + ssh_client_channel:cache_lookup(Cache, ChannelId), + Reply = +- case User of +- undefined -> ++ case session_started(Channel) of ++ false -> + case start_subsystem(SsName, Connection, Channel, + {subsystem, ChannelId, WantReply, binary_to_list(SsName)}) of + {ok, Pid} -> + erlang:monitor(process, Pid), +- ssh_client_channel:cache_update(Cache, Channel#channel{user=Pid}), ++ ssh_client_channel:cache_update(Cache, Channel#channel{user=Pid,sys="subsystem"}), ++ stop_old_channel_handler(User, self()), + channel_success_msg(RemoteId); + {error,_Error} -> + channel_failure_msg(RemoteId) + end; +- _ -> ++ true -> + %% If shell, exec or any subsystem is active on the channel, starting another + %% subsystem must fail. See RFC 4254 6.5 +- %% In our case 'env' (RFC 4254 6.6) and 'pty-req' (RFC 4254 6.2) +- %% (even if any of them failed) also start cli handler, so if any of them are +- %% started, trying to start any subsystem will also fail here. + channel_failure_msg(RemoteId) + end, + {[{connection_reply,Reply}], Connection}; +@@ -1869,20 +1867,22 @@ backwards_compatible([Value| Rest], Acc) -> + handle_cli_msg(C0, ChId, Reply0) -> + Cache = C0#connection.channel_cache, + Ch0 = ssh_client_channel:cache_lookup(Cache, ChId), ++ NewSys = update_sys(Reply0, Ch0#channel.sys), + case Ch0#channel.user of + undefined -> + case start_cli(C0, ChId) of + {ok, Pid} -> + erlang:monitor(process, Pid), +- Ch = Ch0#channel{user = Pid}, ++ Ch = Ch0#channel{user = Pid, sys = NewSys}, + ssh_client_channel:cache_update(Cache, Ch), + reply_msg(Ch, C0, Reply0); + {error, _Error} -> + Reply = {connection_reply, channel_failure_msg(Ch0#channel.remote_id)}, + {[Reply], C0} + end; +- + _ -> ++ Ch = Ch0#channel{sys = NewSys}, ++ ssh_client_channel:cache_update(Cache, Ch), + reply_msg(Ch0, C0, Reply0) + end. + +@@ -1954,3 +1954,37 @@ send_environment_vars(ConnectionRef, Channel, VarNames) -> + Var, Value, infinity) + end + end, success, VarNames). ++ ++%%%---------------------------------------------------------------- ++session_started(#channel{sys = "ssh"}) -> ++ %% No handler started yet ++ false; ++session_started(#channel{sys = Sys}) when Sys =:= "env"; Sys =:= "pty" -> ++ %% cli_handler was started by implicit: ++ %% - 'env' (RFC 4254 6.6) or ++ %% - 'pty-req' (RFC 4254 6.2) ++ %% we don't consider this a started session ++ false; ++session_started(#channel{}) -> ++ %% Some handler is started ++ true. ++ ++%%%---------------------------------------------------------------- ++stop_old_channel_handler(Pid, ConnectionManager) when is_pid(Pid) -> ++ Pid ! {ssh_channel_handler_replaced, ConnectionManager}; ++stop_old_channel_handler(_Other, _ConnectionManager) -> ++ ok. ++ ++%%%---------------------------------------------------------------- ++update_sys(Reply, "ssh") -> ++ %% No handler started yet ++ atom_to_list(element(1, Reply)); ++update_sys(Reply, Sys) when Sys =:= "pty"; Sys =:= "env" -> ++ %% cli_handler was started by implicit: ++ %% - 'env' (RFC 4254 6.6) or ++ %% - 'pty-req' (RFC 4254 6.2) ++ %% we don't consider this a started session ++ atom_to_list(element(1, Reply)); ++update_sys(_Reply, Sys) -> ++ %% Some handler is started ++ Sys. +--- a/lib/ssh/test/ssh_connection_SUITE.erl ++++ b/lib/ssh/test/ssh_connection_SUITE.erl +@@ -1911,6 +1911,7 @@ start_subsystem_on_channel_with_pty_req(Config) -> + DaemonOpts = [{shell, fun(_U, _P) -> spawn(fun() -> timer:sleep(infinity) end) end}], + {ConnectionRef, ChannelId, Pid} = start_server_connect_client_and_get_channel(Config, DaemonOpts), + success = ssh_connection:ptty_alloc(ConnectionRef, ChannelId, []), ++ check_subsystem_replaces_cli_handler(ConnectionRef, ChannelId, Pid), + check_subsystem_start_failure(ConnectionRef, ChannelId, Pid). + + start_subsystem_on_channel_with_shell(Config) -> +@@ -1948,8 +1949,23 @@ start_subsystem_on_channel_with_env(Config) -> + {ConnectionRef, ChannelId, Pid} = start_server_connect_client_and_get_channel(Config, DaemonOpts), + %% setenv is not implemented, and will return a failure, but cli handler will be started anyway + failure = ssh_connection:setenv(ConnectionRef, ChannelId, "ENV_TEST", "VALUE", infinity), ++ check_subsystem_replaces_cli_handler(ConnectionRef, ChannelId, Pid), + check_subsystem_start_failure(ConnectionRef, ChannelId, Pid). + ++check_subsystem_replaces_cli_handler(ConnectionRef, ChannelId, DaemonPid) -> ++ Children0 = supervisor:which_children(DaemonPid), ++ {value, {_, ConnectionSup, supervisor, _}} = lists:keysearch([ssh_connection_sup], 4, Children0), ++ Children1 = supervisor:which_children(ConnectionSup), ++ {value, {_, ChannelSup, supervisor, _}} = lists:keysearch([ssh_channel_sup], 4, Children1), ++ [{_, Pid, worker, [ssh_server_channel]}] = supervisor:which_children(ChannelSup), ++ %% Starting subsystem should replace old cli_handler ++ MRef = erlang:monitor(process, Pid), ++ success = ssh_connection:subsystem(ConnectionRef, ChannelId, "echo_n", 5000), ++ receive ++ {'DOWN', MRef, process, Pid, normal} -> ++ ok ++ end. ++ + check_subsystem_start_failure(ConnectionRef, ChannelId, Pid) -> + failure = ssh_connection:subsystem(ConnectionRef, ChannelId, "echo_n", 5000), + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-69664.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-69664.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-69664.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-69664.patch 2026-09-30 10:13:04.000000000 +0000 @@ -0,0 +1,145 @@ +From: Lukas Backström +Date: Mon, 1 Jun 2026 10:26:29 +0200 +Subject: inets: Fix rejection of invalid chunk sizes + +Before this fix the server would hang indefinitely without the need +to send any bytes to keep it running. This causes it to leak a process +which could be used to DOS the server. + +Origin: upstream, https://github.com/erlang/otp/commit/a3adf63078438c86527d704e23282b7721d8ca12 +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1146603 +--- + .../src/http_server/httpd_request_handler.erl | 22 +++++---- + lib/inets/test/httpd_SUITE.erl | 50 +++++++++++++++++++- + 2 files changed, 63 insertions(+), 9 deletions(-) + +--- a/lib/inets/src/http_server/httpd_request_handler.erl ++++ b/lib/inets/src/http_server/httpd_request_handler.erl +@@ -238,7 +238,6 @@ handle_info({Proto, Socket, Data}, + (Proto =:= ssl) orelse + (Proto =:= dummy)) andalso is_binary(Data)) -> + +- PROCESSED = (catch Module:Function([Data | Args])), + NewDataSize = case State#state.byte_limit of + undefined -> + undefined; +@@ -246,7 +245,7 @@ handle_info({Proto, Socket, Data}, + State#state.data + byte_size(Data) + end, + +- case PROCESSED of ++ try Module:Function([Data | Args]) of + {ok, Result} -> + NewState = case NewDataSize of + undefined -> +@@ -255,13 +254,15 @@ handle_info({Proto, Socket, Data}, + set_new_data_size(cancel_request_timeout(State), NewDataSize) + end, + handle_msg(Result, NewState); ++ + {error, {size_error, MaxSize, ErrCode, ErrStr}, Version} -> + NewModData = ModData#mod{http_version = Version}, + httpd_response:send_status(NewModData, ErrCode, ErrStr, {max_size, MaxSize}), + {stop, normal, State#state{response_sent = true, + mod = NewModData}}; ++ + {error, {version_error, ErrCode, ErrStr}, Version} -> +- NewModData = ModData#mod{http_version = Version}, ++ NewModData = ModData#mod{http_version = Version}, + httpd_response:send_status(NewModData, ErrCode, ErrStr), + {stop, normal, State#state{response_sent = true, + mod = NewModData}}; +@@ -271,17 +272,22 @@ handle_info({Proto, Socket, Data}, + {stop, normal, State#state{response_sent = true, + mod = NewModData}}; + +- {http_chunk = Module, Function, Args} when ChunkState =/= undefined -> +- NewState = handle_chunk(Module, Function, Args, State), +- {noreply, NewState}; +- NewMFA -> +- setopts(Socket, SockType, [{active, once}]), ++ {http_chunk = Module, Function, Args} when ChunkState =/= undefined -> ++ NewState = handle_chunk(Module, Function, Args, State), ++ {noreply, NewState}; ++ ++ {_M, _F, _A} = NewMFA -> ++ setopts(Socket, SockType, [{active, once}]), + case NewDataSize of + undefined -> + {noreply, State#state{mfa = NewMFA}}; + _ -> + {noreply, State#state{mfa = NewMFA, data = NewDataSize}} + end ++ catch throw:{error, Error} when Module =:= http_chunk -> ++ httpd_response:send_status(ModData, 400, ++ "Bad input", {chunk_decoding, bad_input, Error}), ++ {stop, normal, State#state{response_sent = true}} + end; + + %% Error cases +--- a/lib/inets/test/httpd_SUITE.erl ++++ b/lib/inets/test/httpd_SUITE.erl +@@ -127,7 +127,11 @@ groups() -> + disturbing_1_0, + reload_config_file + ]}, +- {post, [], [chunked_post, chunked_chunked_encoded_post, post_204, multiple_content_length_header]}, ++ {post, [], [chunked_post, ++ chunked_chunked_encoded_post, ++ post_204, ++ chunked_invalid_chunk_size, ++ multiple_content_length_header]}, + {basic_auth, [], [basic_auth_1_1, basic_auth_1_0, verify_href_1_1]}, + {auth_api, [], [auth_api_1_1, auth_api_1_0]}, + {auth_api_dets, [], [auth_api_1_1, auth_api_1_0]}, +@@ -899,6 +903,50 @@ post_204(Config) -> + {args, [SockType, Host, Port, TranspOpts]}]}) + end. + ++%% This test used to make httpd hang ++chunked_invalid_chunk_size(Config) -> ++ Host = proplists:get_value(host, Config), ++ Port = proplists:get_value(port, Config), ++ SockType = proplists:get_value(type, Config), ++ TranspOpts = transport_opts(SockType, Config), ++ try inets_test_lib:connect_bin(SockType, Host, Port, TranspOpts) of ++ {ok, Socket} -> ++ RequestStr = "POST /cgi-bin/erl/httpd_example:post_chunked HTTP/1.1\r\n" ++ ++ "Host: " ++ Host ++ "\r\n" ++ ++ "Transfer-Encoding: chunked\r\n" ++ ++ "\r\n", ++ io:format("Sending request with invalid chunked encoding: '~p'~n", [RequestStr]), ++ ok = inets_test_lib:send(SockType, Socket, RequestStr), ++ receive ++ {tcp, Socket, Data} -> ++ io:format("Received response: '~p'~n", [Data]), ++ ct:fail("Expected server to not send a response yet.") ++ after 1000 -> ++ ok ++ end, ++ io:format("Sending request with too large header: '~p'~n", ["zz\r\n"]), ++ ok = inets_test_lib:send(SockType, Socket, "zz\r\n"), ++ receive ++ {tcp, Socket, Data2} -> ++ io:format("Received response: '~p'~n", [Data2]), ++ case binary:match(Data2, <<"400">>,[]) of ++ nomatch -> ++ ct:fail("Expected 400 Bad Request response."); ++ {_, _} -> ++ ok ++ end ++ after 2000 -> ++ ct:fail(connection_timed_out) ++ end ++ catch ++ T:E:Stk -> ++ ct:fail({connect_failure, ++ [{type, T}, ++ {error, E}, ++ {stacktrace, Stk}, ++ {args, [SockType, Host, Port, TranspOpts]}]}) ++ end. ++ + %%------------------------------------------------------------------------- + host() -> + [{doc, "Test host header"}]. diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70399.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70399.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70399.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70399.patch 2026-09-30 10:13:04.000000000 +0000 @@ -0,0 +1,286 @@ +From: Lukas Backström +Date: Mon, 1 Jun 2026 16:29:47 +0200 +Subject: inets: Fix default max_clients + +The documentation specified the max_clients to be 150 by default, +but the implementation did not respect that default, so it allowed +an infinite amount of clients. + +Origin: upstream, https://github.com/erlang/otp/commit/d94a94c96eb4cccbd6f7d7da5975f0c035b69612 +Bug-Debian: https://bugs.debian.org/1146603 +Comment: httpd_basic_SUITE max_clients_default hunk re-anchored after + CVE-2026-74835-1.patch (chunked_body_size_unbounded test), placed as in + upstream merge 1b7fe21f15. +--- + lib/inets/src/http_lib/http_internal.hrl | 1 + + lib/inets/src/http_server/httpd_manager.erl | 47 +++----------- + lib/inets/test/httpd_SUITE.erl | 68 ++++++++++++++------ + lib/inets/test/httpd_basic_SUITE.erl | 16 ++++- + 4 files changed, 73 insertions(+), 59 deletions(-) + +--- a/lib/inets/src/http_lib/http_internal.hrl ++++ b/lib/inets/src/http_lib/http_internal.hrl +@@ -30,6 +30,7 @@ + -define(HTTP_MAX_VERSION_STRING, 8). + -define(HTTP_MAX_METHOD_STRING, 20). + -define(HTTP_MAX_CONTENT_LENGTH, 100000000). ++-define(HTTP_MAX_CLIENTS, 150). + + %%% Response headers + -record(http_response_h,{ +--- a/lib/inets/src/http_server/httpd_manager.erl ++++ b/lib/inets/src/http_server/httpd_manager.erl +@@ -23,6 +23,7 @@ + -moduledoc false. + + -include("httpd.hrl"). ++-include("../http_lib/http_internal.hrl"). + + -behaviour(gen_server). + +@@ -225,8 +226,8 @@ handle_call({unblock, Blocker}, _, #state{blocker_ref = {Blocker, Monitor}, + handle_call({unblock, _}, _, State) -> + {reply, {error, only_blocker_may_unblock}, State}; + +-handle_call({new_connection, Pid}, _From, State) -> +- {Status, NewState} = handle_new_connection(State, Pid), ++handle_call({new_connection, _Pid}, _From, State) -> ++ {Status, NewState} = handle_new_connection(State), + {reply, Status, NewState}; + + handle_call(Request, From, State) -> +@@ -318,22 +319,16 @@ code_change(_FromVsn, State, _Extra) -> + %%%-------------------------------------------------------------------- + %%% Internal functions + %%%-------------------------------------------------------------------- +-handle_new_connection(#state{admin_state = AdminState} = State, Handler) -> +- UsageState = get_ustate(State), +- handle_new_connection(UsageState, AdminState, State, Handler). +- +-handle_new_connection(_UsageState, unblocked, +- #state{config_db = Db, connection_sup = CSup} = +- State, _) -> +- Max = httpd_util:lookup(Db, max_clients), +- case count_children(CSup) of +- Count when Count =< Max -> +- {{ok, accept}, State}; ++handle_new_connection(#state{admin_state = unblocked, config_db = Db} = State) -> ++ Children = count_children(State#state.connection_sup), ++ case httpd_util:lookup(Db, max_clients, ?HTTP_MAX_CLIENTS) of ++ MaxClients when MaxClients < Children -> ++ {{reject, busy}, State}; + _ -> +- {{reject, busy}, State} ++ {{ok, accept}, State} + end; + +-handle_new_connection(_UsageState, _AdminState, State, _Handler) -> ++handle_new_connection(State) -> + {{reject, blocked}, State}. + + handle_block(disturbing, infinity, +@@ -420,28 +415,6 @@ check_constant_values(Db, Config) -> + end, + ok. + +- +-%% get_ustate(State) -> idle | active | busy +-%% +-%% Retrieve the usage state of the HTTP server: +-%% 0 active connection -> idle +-%% max_clients active connections -> busy +-%% Otherwise -> active +-%% +-get_ustate(State) -> +- get_ustate(count_children(State#state.connection_sup),State). +- +-get_ustate(0,_State) -> +- idle; +-get_ustate(ConnectionCnt,State) -> +- ConfigDB = State#state.config_db, +- case httpd_util:lookup(ConfigDB, max_clients, 150) of +- ConnectionCnt -> +- busy; +- _ -> +- active +- end. +- + make_name(Addr, Port, Profile) -> + httpd_util:make_name("httpd", Addr, Port, Profile). + +--- a/lib/inets/test/httpd_SUITE.erl ++++ b/lib/inets/test/httpd_SUITE.erl +@@ -189,6 +189,7 @@ load() -> + ]. + + init_per_suite(Config) -> ++ + PrivDir = proplists:get_value(priv_dir, Config), + DataDir = proplists:get_value(data_dir, Config), + inets_test_lib:stop_apps([inets]), +@@ -1067,7 +1068,7 @@ max_clients_1_1() -> + [{doc, "Test max clients limit"}]. + + max_clients_1_1(Config) when is_list(Config) -> +- do_max_clients([{http_version, "HTTP/1.1"} | Config]). ++ do_max_clients([{http_version, "HTTP/1.1"} | Config], 1). + + %%------------------------------------------------------------------------- + put_not_sup() -> +@@ -2269,16 +2270,27 @@ verify_timeout(Info, Expected) -> + ct:fail("Bad Timeout - Expected: ~p Got: ~p", [Expected, Timeout]) + end. + +-do_max_clients(Config) -> ++do_max_clients(Config, MaxClients) -> + Version = proplists:get_value(http_version, Config), + Host = proplists:get_value(host, Config), + Port = proplists:get_value(port, Config), + Type = proplists:get_value(type, Config), + + Request = http_request("GET /index.html ", Version, Host), +- BlockRequest = http_request("GET /cgi_bin/erl/httpd_example:delay ", Version, Host), +- {ok, Socket} = inets_test_lib:connect_bin(Type, Host, Port, transport_opts(Type, Config)), +- inets_test_lib:send(Type, Socket, BlockRequest), ++ BlockRequest = "GET /index.html " ++ Version ++ "\r\n", ++ ++ Connections = ++ [begin ++ {ok, Socket} = inets_test_lib:connect_bin(Type, Host, Port, transport_opts(Type, Config)), ++ inets_test_lib:send(Type, Socket, BlockRequest), ++ Socket ++ end || _ <- lists:seq(1, MaxClients)], ++ ++ wait_for_N(fun connection_children/0, MaxClients, 5000), ++ ++ [ct:log("~p: ~p", [Name, supervisor:count_children(whereis(Name))]) || Name <- registered(), ++ lists:prefix("httpd_connection_sup", atom_to_list(Name))], ++ + ok = httpd_test_lib:verify_request(Type, Host, + Port, + transport_opts(Type, Config), +@@ -2286,12 +2298,11 @@ do_max_clients(Config) -> + Request, + [{statuscode, 503}, + {version, Version}]), +- receive +- {_, Socket, _Msg} -> +- ok +- end, +- inets_test_lib:close(Type, Socket), +- ct:sleep(5000), %% Avoid possible timing issues ++ ++ [ inets_test_lib:close(Type, Socket) || Socket <- Connections ], ++ ++ wait_for_N(fun connection_children/0, 0, 5000), ++ + ok = httpd_test_lib:verify_request(Type, Host, + Port, + transport_opts(Type, Config), +@@ -2300,6 +2311,24 @@ do_max_clients(Config) -> + [{statuscode, 200}, + {version, Version}]). + ++connection_children() -> ++ lists:sum([proplists:get_value(workers, supervisor:count_children(whereis(Name))) || Name <- registered(), ++ lists:prefix("httpd_connection_sup", atom_to_list(Name))]). ++ ++wait_for_N(Fun, N, Timeout) -> ++ case Fun() of ++ N -> ++ ok; ++ _ -> ++ case Timeout > 0 of ++ true -> ++ ct:sleep(100), ++ wait_for_N(Fun, N, Timeout - 100); ++ false -> ++ ct:fail(timeout) ++ end ++ end. ++ + setup_server_dirs(ServerRoot, DocRoot, DataDir) -> + CgiDir = filename:join(ServerRoot, "cgi-bin"), + ExtCgiDir = filename:join(ServerRoot, "ext-cgi-bin"), +@@ -2439,20 +2468,16 @@ server_config(http_post, Config) -> + server_config(https_reload, Config) -> + [{keep_alive_timeout, 2}] ++ server_config(https, Config); + server_config(http_limit, Config) -> +- Conf = [{max_clients, 1}, +- {disable_chunked_transfer_encoding_send, true}, +- %% Make sure option checking code is run +- {max_content_length, 100000002}] ++ server_config(http, Config), +- ct:log("Received message ~p~n", [Conf]), +- Conf; ++ [{max_clients, 1}, ++ {disable_chunked_transfer_encoding_send, true}, ++ %% Make sure option checking code is run ++ {max_content_length, 100000002}] ++ server_config(http, Config); + server_config(http_custom, Config) -> + [{customize, ?MODULE}] ++ server_config(http, Config); + server_config(https_custom, Config) -> + [{customize, ?MODULE}] ++ server_config(https, Config); + server_config(https_limit, Config) -> +- [{max_clients, 1}, +- {disable_chunked_transfer_encoding_send, true} +- ] ++ server_config(https, Config); ++ [{max_clients, 1},{disable_chunked_transfer_encoding_send, true}] ++ server_config(https, Config); + server_config(http_basic_auth, Config) -> + ServerRoot = proplists:get_value(server_root, Config), + auth_conf(ServerRoot) ++ server_config(http, Config); +@@ -2541,6 +2566,7 @@ config_template(Config, ServerRoot, ScriptPath, Modules) -> + {ipfamily, proplists:get_value(ipfamily, Config)}, + {max_header_size, 256}, + {max_header_action, close}, ++ {max_clients, proplists:get_value(max_clients, Config, 1000)}, + {directory_index, ["index.html", "welcome.html"]}, + {mime_types, [{"html","text/html"},{"htm","text/html"}, {"shtml","text/html"}, + {"gif", "image/gif"}]}, +@@ -2847,7 +2873,7 @@ transport_opts(ssl, Config) -> + ClientConf = proplists:get_value(client_config, SSLConf), + [proplists:get_value(ipfamily, Config) | ClientConf]; + transport_opts(_, Config) -> +- [proplists:get_value(ipfamily, Config)]. ++ [proplists:get_value(ipfamily, Config, inet)]. + + + %%% mod_range +--- a/lib/inets/test/httpd_basic_SUITE.erl ++++ b/lib/inets/test/httpd_basic_SUITE.erl +@@ -34,7 +34,7 @@ suite() -> [{ct_hooks,[ts_install_cth]}, + {timetrap, {seconds, 30}}]. + + all() -> +- [{group, httpd_basic}]. ++ [{group, httpd_basic}, max_clients_default]. + + groups() -> + [{httpd_basic, [parallel], [uri_too_long_414, +@@ -101,6 +101,7 @@ DUMMY + {ok, Fd} = file:open(DummyFile, [write]), + ok = file:write(Fd, Dummy), + ok = file:close(Fd), ++ {ok,_} = file:copy(DummyFile, filename:join([PrivDir,"index.html"])), + HttpdConf = [{port, 0}, + {ipfamily, inet}, + {server_name, "httpd_test"}, +@@ -403,6 +404,19 @@ invalid_rfc1123_date(Config) when is_list(Config) -> + NonDSTDateTime = {{2017, 03, 26},{1, 0, 0}}, + Rfc1123FormattedDate =:= httpd_util:rfc1123_date(NonDSTDateTime). + ++%%------------------------------------------------------------------------- ++ ++max_clients_default(Config) when is_list(Config) -> ++ HttpdConf = proplists:get_value(httpd_conf, Config), ++ {ok, Pid} = inets:start(httpd, [{port, 0} | HttpdConf]), ++ Port = proplists:get_value(port, httpd:info(Pid)), ++ HttpdClient = [{host, "localhost"}, {type, ip_comm}, {port, Port} | Config], ++ try ++ httpd_SUITE:do_max_clients([{http_version, "HTTP/1.1"} | HttpdClient], 150), ++ httpd_SUITE:do_max_clients([{http_version, "HTTP/1.0"} | HttpdClient], 150) ++ after ++ inets:stop(httpd, Pid) ++ end. + + %%------------------------------------------------------------------------- + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70405-followup.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70405-followup.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70405-followup.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70405-followup.patch 2026-10-20 16:55:29.000000000 +0000 @@ -0,0 +1,631 @@ +From: Erlang/OTP +Date: Wed, 14 Oct 2026 14:38:07 +0200 +Subject: Merge branch 'whaileee/snmp/ber-integer-limits/patch-base-27/OTP-20428' into maint-27 + +* whaileee/snmp/ber-integer-limits/patch-base-27/OTP-20428: + snmp: Bound integer parsing in the BER decoder + +# Conflicts: +# lib/snmp/src/misc/snmp_pdus.erl + +Origin: upstream, https://github.com/erlang/otp/commit/787840115f9ead11559a90c0d0aa61ddf4e959b7 +--- + lib/snmp/src/misc/snmp_pdus.erl | 72 ++++--- + lib/snmp/test/snmp_pdus_SUITE.erl | 388 +++++++++++++++++++++++++++++++++++++- + 2 files changed, 429 insertions(+), 31 deletions(-) + +--- a/lib/snmp/src/misc/snmp_pdus.erl ++++ b/lib/snmp/src/misc/snmp_pdus.erl +@@ -47,6 +47,16 @@ process. + -define(VMODULE,"PDUS"). + -include("snmp_verbosity.hrl"). + ++%% Maximum number of content octets accepted for a BER-encoded integer. ++%% Each value is the minimal BER length of the largest legal value of the ++%% type. Unsigned types need one octet more than their width, for the ++%% leading zero that keeps the value positive. ++-define(INT32_MAX_OCTETS, 4). %% ASN.1 INTEGER / Integer32 and every ++ %% INTEGER (0..2147483647) field ++-define(UINT32_MAX_OCTETS, 5). %% Counter32, Gauge32/Unsigned32, TimeTicks ++-define(UINT64_MAX_OCTETS, 9). %% Counter64 ++-define(VERSION_MAX_OCTETS, 10). %% msgVersion -- deliberately generous ++ + %% See RFC1155, RFC1157, RFC1901, RFC1902, RFC1905, RFC2272 + + +@@ -152,11 +162,11 @@ dec_message_only([48 | Bytes]) -> + end. + + dec_snmp_version(Bytes) -> +- case (catch dec_int_tag(Bytes, 10)) of +- {error, {bad_integer, BadInt}} -> +- exit({bad_version, BadInt}); ++ case (catch dec_int_tag(Bytes, ?VERSION_MAX_OCTETS)) of + {SNMPversion, Rest} when is_integer(SNMPversion) andalso is_list(Rest) -> + {dec_snmp_ver(SNMPversion), Rest}; ++ {'EXIT', {error, {bad_integer, BadInt}}} -> ++ exit({bad_version, BadInt}); + {'EXIT', Reason} -> + exit(Reason) + end. +@@ -184,12 +194,18 @@ dec_rest_v3_msg_only([48 | Bytes]) -> % + {Size, Tail} = dec_len(Bytes), + {HBytes, Bytes1} = split_at(Tail, Size, []), + %% Decode HeaderData +- {MsgID, HBytes1} = dec_int_tag(HBytes), ++ {MsgID, HBytes1} = try dec_int_tag(HBytes, ?INT32_MAX_OCTETS) ++ catch exit:{error, {bad_integer, BadSizeMsgId}} -> exit({bad_msg_id_bytesize, BadSizeMsgId}) ++ end, + chk_msg_id(MsgID), +- {MsgMaxSize, HBytes2} = dec_int_tag(HBytes1), ++ {MsgMaxSize, HBytes2} = try dec_int_tag(HBytes1, ?INT32_MAX_OCTETS) ++ catch exit:{error, {bad_integer, BadSizeMaxSize}} -> exit({bad_msg_max_size_bytesize, BadSizeMaxSize}) ++ end, + chk_msg_max_size(MsgMaxSize), + {MsgFlags, HBytes3} = dec_oct_str_tag(HBytes2), +- {MsgSecurityModel, []} = dec_int_tag(HBytes3), ++ {MsgSecurityModel, []} = try dec_int_tag(HBytes3, ?INT32_MAX_OCTETS) ++ catch exit:{error, {bad_integer, BadSizeSecModel}} -> exit({bad_sec_model_bytesize, BadSizeSecModel}) ++ end, + chk_msg_sec_model(MsgSecurityModel), + %% Continue with Message + % {MsgSecurityParameters, Bytes2} = dec_oct_str_tag(Bytes1), +@@ -281,8 +297,8 @@ dec_pdu([164 | Bytes]) -> % It's a + Bytes2 = get_data_bytes(Bytes), + {Enterprise, Rest1} = dec_oid_tag(Bytes2), + {{'IpAddress', [_, _, _, _] = AgentAddr}, Rest2} = dec_value(Rest1), +- {GenericTrap, Rest3} = dec_int_tag(Rest2), +- {SpecificTrap, Rest4} = dec_int_tag(Rest3), ++ {GenericTrap, Rest3} = dec_int_tag(Rest2, ?INT32_MAX_OCTETS), ++ {SpecificTrap, Rest4} = dec_int_tag(Rest3, ?INT32_MAX_OCTETS), + {{'TimeTicks', TimeStamp}, VBBytes} = dec_value(Rest4), + VBs = dec_VBs(VBBytes), + #trappdu{enterprise = Enterprise, agent_addr = AgentAddr, +@@ -291,9 +307,9 @@ dec_pdu([164 | Bytes]) -> % It's a + + dec_pdu([165 | Bytes]) -> % Bulk + Bytes2 = get_data_bytes(Bytes), +- {RequestID, Rest1} = dec_int_tag(Bytes2), +- {NonRepeaters, Rest2} = dec_int_tag(Rest1), +- {MaxRepetitions,VBbytes} = dec_int_tag(Rest2), ++ {RequestID, Rest1} = dec_int_tag(Bytes2, ?INT32_MAX_OCTETS), ++ {NonRepeaters, Rest2} = dec_int_tag(Rest1, ?INT32_MAX_OCTETS), ++ {MaxRepetitions,VBbytes} = dec_int_tag(Rest2, ?INT32_MAX_OCTETS), + VBs = dec_VBs(VBbytes), + #pdu{type = 'get-bulk-request', request_id = RequestID, + error_status = NonRepeaters, error_index = MaxRepetitions, +@@ -302,15 +318,15 @@ dec_pdu([165 | Bytes]) -> % Bulk + dec_pdu([PduTag | Bytes]) -> + Type = dec_pdu_tag(PduTag), + Bytes2 = get_data_bytes(Bytes), +- {RequestID, Rest1} = dec_int_tag(Bytes2), +- {ErrStat, Rest2} = dec_int_tag(Rest1), ++ {RequestID, Rest1} = dec_int_tag(Bytes2, ?INT32_MAX_OCTETS), ++ {ErrStat, Rest2} = dec_int_tag(Rest1, ?INT32_MAX_OCTETS), + ErrStatus = case lists:keysearch(ErrStat, 2, errMsgs()) of + {value, {ErrStatName, _ErrStat}} -> + ErrStatName; + false -> + ErrStat + end, +- {ErrIndex, VarbindsBytes} = dec_int_tag(Rest2), ++ {ErrIndex, VarbindsBytes} = dec_int_tag(Rest2, ?INT32_MAX_OCTETS), + VBs = dec_VBs(VarbindsBytes), + #pdu{type = Type, request_id = RequestID, error_status = ErrStatus, + error_index = ErrIndex, varbinds = VBs}. +@@ -342,8 +358,14 @@ dec_individual_VBs([48 | Bytes], OrgInde + dec_usm_security_parameters([48 | Bytes1]) -> + {_Len, Bytes2} = dec_len(Bytes1), + {MsgAuthEngineID, Bytes3} = dec_oct_str_tag(Bytes2), +- {MsgAuthEngineBoots, Bytes4} = dec_int_tag(Bytes3), +- {MsgAuthEngineTime, Bytes5} = dec_int_tag(Bytes4), ++ {MsgAuthEngineBoots, Bytes4} = try dec_int_tag(Bytes3, ?INT32_MAX_OCTETS) ++ catch ++ exit:{error, {bad_integer, SizeBoots}} -> exit({bad_engine_boots_bytesize, SizeBoots}) ++ end, ++ {MsgAuthEngineTime, Bytes5} = try dec_int_tag(Bytes4, ?INT32_MAX_OCTETS) ++ catch ++ exit:{error, {bad_integer, SizeTime}} -> exit({bad_engine_time_bytesize, SizeTime}) ++ end, + {MsgUserName, Bytes6} = dec_oct_str_tag(Bytes5), + {MsgAuthParams, Bytes7} = dec_oct_str_tag(Bytes6), + {MsgPrivParams, []} = dec_oct_str_tag(Bytes7), +@@ -378,7 +400,7 @@ dec_value([5,0 | T]) -> + + %% INTEGER + dec_value([2 | Bytes]) -> +- {Value, Rest} = dec_integer_notag(Bytes, 4), %% SMI INTEGER is 32-bit ++ {Value, Rest} = dec_integer_notag(Bytes, ?INT32_MAX_OCTETS), %% SMI INTEGER is 32-bit + {{'INTEGER', Value}, Rest}; + + %% OCTET STRING +@@ -395,7 +417,7 @@ dec_value([64 | Bytes]) -> + dec_value([65 | Bytes]) -> + %% Counter32 is an unsigned 32 but is actually encoded as + %% a signed integer 32 (INTEGER). +- {Value, Rest} = dec_integer_notag(Bytes, 5), ++ {Value, Rest} = dec_integer_notag(Bytes, ?UINT32_MAX_OCTETS), + Value2 = + if + (Value >= 0) andalso (Value =< 16#ffffffff) -> +@@ -411,7 +433,7 @@ dec_value([65 | Bytes]) -> + + %% Unsigned32 + dec_value([66 | Bytes]) -> +- {Value, Rest} = dec_integer_notag(Bytes, 5), ++ {Value, Rest} = dec_integer_notag(Bytes, ?UINT32_MAX_OCTETS), + Value2 = + if + (Value >= 0) andalso (Value =< 16#ffffffff) -> +@@ -425,7 +447,7 @@ dec_value([66 | Bytes]) -> + + %% TimeTicks + dec_value([67 | Bytes]) -> +- {Value, Rest} = dec_integer_notag(Bytes, 5), ++ {Value, Rest} = dec_integer_notag(Bytes, ?UINT32_MAX_OCTETS), + Value2 = + if + (Value >= 0) andalso (Value =< 16#ffffffff) -> +@@ -446,7 +468,7 @@ dec_value([68 | Bytes]) -> + dec_value([70 | Bytes]) -> + %% Counter64 is an unsigned 64 but is actually encoded as + %% a signed integer 64. +- {Value, Rest} = dec_integer_notag(Bytes, 9), ++ {Value, Rest} = dec_integer_notag(Bytes, ?UINT64_MAX_OCTETS), + Value2 = + if + (Value >= 0) andalso (Value < 16#8000000000000000) -> +@@ -488,22 +510,16 @@ split_at([H|T], N, Acc) -> + %% All decoding routines return: {Data, RestBytes} + %%---------------------------------------------------------------------- + +-dec_int_tag([2 | Bytes]) -> +- dec_integer_notag(Bytes). + dec_int_tag([2 | Bytes], SizeLimit) -> + dec_integer_notag(Bytes, SizeLimit). + +-dec_integer_notag(Ints) -> +- dec_integer_notag(Ints, infinity). + dec_integer_notag(Ints, SizeLimit) -> + case dec_len(Ints) of +- {Size, Ints2} when SizeLimit =:= infinity -> +- do_dec_integer_notag(Size, Ints2); + {Size, Ints2} when (is_integer(SizeLimit) andalso + (Size =< SizeLimit)) -> + do_dec_integer_notag(Size, Ints2); + {BadSize, _BadInts2} -> +- throw({error, {bad_integer, {BadSize, SizeLimit}}}) ++ exit({error, {bad_integer, {BadSize, SizeLimit}}}) + end. + + do_dec_integer_notag(Size, Ints) -> +--- a/lib/snmp/test/snmp_pdus_SUITE.erl ++++ b/lib/snmp/test/snmp_pdus_SUITE.erl +@@ -29,6 +29,7 @@ + %%---------------------------------------------------------------------- + -include_lib("common_test/include/ct.hrl"). + -include("snmp_test_lib.hrl"). ++-define(SNMP_USE_V3, true). + -include_lib("snmp/include/snmp_types.hrl"). + + +@@ -45,7 +46,13 @@ + otp7575/1, + otp8563/1, + otp9022/1, +- otp10132/1 ++ otp10132/1, ++ ++ dec_value_int_size_limits/1, ++ dec_pdu_int_size_limits/1, ++ dec_v3_hdr_int_size_limits/1, ++ dec_v3_hdr_value_checks/1, ++ dec_usm_sec_params_int_size_limits/1 + ]). + + +@@ -57,10 +64,12 @@ suite() -> + [{ct_hooks, [ts_install_cth]}]. + + all() -> +- [{group, tickets}]. ++ [{group, tickets}, ++ {group, ber_limits}]. + + groups() -> +- [{tickets, [], tickets_cases()}]. ++ [{tickets, [], tickets_cases()}, ++ {ber_limits, [], ber_limits_cases()}]. + + tickets_cases() -> + [ +@@ -70,6 +79,15 @@ tickets_cases() -> + otp10132 + ]. + ++ber_limits_cases() -> ++ [ ++ dec_value_int_size_limits, ++ dec_pdu_int_size_limits, ++ dec_v3_hdr_int_size_limits, ++ dec_v3_hdr_value_checks, ++ dec_usm_sec_params_int_size_limits ++ ]. ++ + + %% + %% ----- +@@ -388,9 +406,373 @@ otp10132(Config) when is_list(Config) -> + + + %%====================================================================== ++%% BER integer content octet limits ++%%====================================================================== ++ ++%% snmp_pdus caps the number of BER content octets accepted for every ++%% integer-valued field (?INT32_MAX_OCTETS and friends). The cases below ++%% pin, for each field, the largest accepted encoding and the rejection of ++%% one octet more. ++%% ++%% They also pin that the rejection is an *exit*. It used to be a throw, ++%% which slipped through the (catch ...) wrappers in snmpa_mpd -- e.g. ++%% around dec_scoped_pdu/1 in v3_proc/6 -- and produced a case_clause ++%% instead of a discarded packet. dec_class/1 reports the exception class, ++%% so a regression back to throw fails the case instead of passing ++%% silently. ++ ++dec_value_int_size_limits(suite) -> []; ++dec_value_int_size_limits(doc) -> ++ ["Content octet limits for the integer types decoded by dec_value/1"]; ++dec_value_int_size_limits(Config) when is_list(Config) -> ++ %% {Type, BER tag, max content octets}. The unsigned types allow one ++ %% octet more than their width, for the leading zero that keeps the ++ %% value positive. ++ Types = [{'INTEGER', 2, 4}, ++ {'Counter32', 65, 5}, ++ {'Unsigned32', 66, 5}, ++ {'TimeTicks', 67, 5}, ++ {'Counter64', 70, 9}], ++ lists:foreach( ++ fun({Type, Tag, Max}) -> ++ ?IPRINT("~w: ~w content octets is accepted", [Type, Max]), ++ case snmp_pdus:dec_value(padded_value(Tag, Max)) of ++ {{Type, 1}, []} -> ++ ok; ++ UnexpectedOk -> ++ exit({unexpected_decode_result, Type, Max, UnexpectedOk}) ++ end, ++ ++ ?IPRINT("~w: ~w content octets is rejected", [Type, Max + 1]), ++ case dec_class(fun() -> ++ snmp_pdus:dec_value( ++ padded_value(Tag, Max + 1)) ++ end) of ++ {exit, {error, {bad_integer, {Bad, Max}}}} when Bad =:= Max + 1 -> ++ ok; ++ Unexpected -> ++ exit({unexpected_decode_result, Type, Max + 1, Unexpected}) ++ end ++ end, Types), ++ ?IPRINT("done"), ++ ok. ++ ++ ++dec_pdu_int_size_limits(suite) -> []; ++dec_pdu_int_size_limits(doc) -> ++ ["Content octet limits for the integer fields of PDU, BulkPDU and TrapPDU"]; ++dec_pdu_int_size_limits(Config) when is_list(Config) -> ++ %% RFC 3416: request-id is INTEGER (-2147483648..2147483647) and ++ %% error-index, non-repeaters and max-repetitions are ++ %% INTEGER (0..max-bindings) with max-bindings = 2147483647 -- so four ++ %% content octets each. ++ ?IPRINT("get-request: a well formed PDU decodes"), ++ #pdu{type = 'get-request', ++ request_id = 1, ++ error_status = noError, ++ error_index = 0, ++ varbinds = []} = ++ snmp_pdus:dec_pdu(pdu(160, [int_tlv(1, 4), ++ int_tlv(0, 1), ++ int_tlv(0, 1)])), ++ ++ ?IPRINT("get-request: request-id with 5 content octets is rejected"), ++ expect_bad_integer(request_id, {5, 4}, ++ fun() -> ++ snmp_pdus:dec_pdu(pdu(160, [int_tlv(1, 5), ++ int_tlv(0, 1), ++ int_tlv(0, 1)])) ++ end), ++ ++ %% error-status is an enumerated INTEGER (noError(0)..inconsistentName(18)), ++ %% so one content octet carries every legal value. The exact cap is not ++ %% pinned here -- only that a grossly oversized encoding is rejected -- ++ %% because unrecognised values are deliberately passed through by ++ %% dec_pdu/1 and the cap may be tightened without changing behaviour. ++ ?IPRINT("get-request: error-status with 8 content octets is rejected"), ++ case dec_class(fun() -> ++ snmp_pdus:dec_pdu(pdu(160, [int_tlv(1, 1), ++ int_tlv(0, 8), ++ int_tlv(0, 1)])) ++ end) of ++ {exit, {error, {bad_integer, {8, _}}}} -> ++ ok; ++ UnexpectedErrStatus -> ++ exit({unexpected_decode_result, error_status, UnexpectedErrStatus}) ++ end, ++ ++ ?IPRINT("get-request: error-index with 5 content octets is rejected"), ++ expect_bad_integer(error_index, {5, 4}, ++ fun() -> ++ snmp_pdus:dec_pdu(pdu(160, [int_tlv(1, 1), ++ int_tlv(0, 1), ++ int_tlv(0, 5)])) ++ end), ++ ++ ?IPRINT("get-bulk-request: a well formed PDU decodes"), ++ #pdu{type = 'get-bulk-request', ++ request_id = 1, ++ error_status = 0, ++ error_index = 10, ++ varbinds = []} = ++ snmp_pdus:dec_pdu(pdu(165, [int_tlv(1, 4), ++ int_tlv(0, 1), ++ int_tlv(10, 1)])), ++ ++ ?IPRINT("get-bulk-request: non-repeaters with 5 content octets is rejected"), ++ expect_bad_integer(non_repeaters, {5, 4}, ++ fun() -> ++ snmp_pdus:dec_pdu(pdu(165, [int_tlv(1, 1), ++ int_tlv(0, 5), ++ int_tlv(0, 1)])) ++ end), ++ ++ ?IPRINT("get-bulk-request: max-repetitions with 5 content octets is rejected"), ++ expect_bad_integer(max_repetitions, {5, 4}, ++ fun() -> ++ snmp_pdus:dec_pdu(pdu(165, [int_tlv(1, 1), ++ int_tlv(0, 1), ++ int_tlv(0, 5)])) ++ end), ++ ++ ?IPRINT("trap: a well formed TrapPDU decodes"), ++ #trappdu{generic_trap = 6, ++ specific_trap = 1, ++ time_stamp = 0, ++ varbinds = []} = ++ snmp_pdus:dec_pdu(trap_pdu(int_tlv(6, 1), int_tlv(1, 4))), ++ ++ ?IPRINT("trap: generic-trap with 5 content octets is rejected"), ++ expect_bad_integer(generic_trap, {5, 4}, ++ fun() -> ++ snmp_pdus:dec_pdu( ++ trap_pdu(int_tlv(6, 5), int_tlv(1, 1))) ++ end), ++ ++ ?IPRINT("trap: specific-trap with 5 content octets is rejected"), ++ expect_bad_integer(specific_trap, {5, 4}, ++ fun() -> ++ snmp_pdus:dec_pdu( ++ trap_pdu(int_tlv(6, 1), int_tlv(1, 5))) ++ end), ++ ++ ?IPRINT("done"), ++ ok. ++ ++ ++dec_v3_hdr_int_size_limits(suite) -> []; ++dec_v3_hdr_int_size_limits(doc) -> ++ ["Content octet limits for the v3 header data integer fields"]; ++dec_v3_hdr_int_size_limits(Config) when is_list(Config) -> ++ %% RFC 3412: msgID and msgMaxSize are INTEGER (0..2147483647) and ++ %% msgSecurityModel is INTEGER (1..2147483647) -- four content octets. ++ ?IPRINT("the largest legal msgID is accepted"), ++ #message{version = 'version-3', ++ vsn_hdr = #v3_hdr{msgID = 2147483647, ++ msgMaxSize = 484, ++ msgSecurityModel = 3}} = ++ snmp_pdus:dec_message_only(v3_msg([raw_int_tlv([127, 255, 255, 255]), ++ v3_msg_max_size_tlv(), ++ v3_msg_flags_tlv(), ++ v3_sec_model_tlv()])), ++ ++ ?IPRINT("msgID with 5 content octets is rejected"), ++ expect_exit(msg_id, {bad_msg_id_bytesize, {5, 4}}, ++ fun() -> ++ snmp_pdus:dec_message_only( ++ v3_msg([int_tlv(1, 5), ++ v3_msg_max_size_tlv(), ++ v3_msg_flags_tlv(), ++ v3_sec_model_tlv()])) ++ end), ++ ++ ?IPRINT("msgMaxSize with 5 content octets is rejected"), ++ expect_exit(msg_max_size, {bad_msg_max_size_bytesize, {5, 4}}, ++ fun() -> ++ snmp_pdus:dec_message_only( ++ v3_msg([int_tlv(1, 1), ++ int_tlv(1, 5), ++ v3_msg_flags_tlv(), ++ v3_sec_model_tlv()])) ++ end), ++ ++ ?IPRINT("msgSecurityModel with 5 content octets is rejected"), ++ expect_exit(msg_sec_model, {bad_sec_model_bytesize, {5, 4}}, ++ fun() -> ++ snmp_pdus:dec_message_only( ++ v3_msg([int_tlv(1, 1), ++ v3_msg_max_size_tlv(), ++ v3_msg_flags_tlv(), ++ int_tlv(1, 5)])) ++ end), ++ ++ ?IPRINT("done"), ++ ok. ++ ++ ++dec_v3_hdr_value_checks(suite) -> []; ++dec_v3_hdr_value_checks(doc) -> ++ ["Range checks on the v3 header data integer fields are applied in " ++ "addition to the content octet limits"]; ++dec_v3_hdr_value_checks(Config) when is_list(Config) -> ++ %% A content octet limit is not a range check: four octets still encode ++ %% 16#80000000, and one octet still encodes a msgMaxSize below the ++ %% RFC 3412 floor of 484. chk_msg_id/1, chk_msg_max_size/1 and ++ %% chk_msg_sec_model/1 must keep rejecting those. ++ ?IPRINT("a negative msgID is rejected"), ++ expect_exit(msg_id, {bad_msg_id, -2147483648}, ++ fun() -> ++ snmp_pdus:dec_message_only( ++ v3_msg([raw_int_tlv([128, 0, 0, 0]), ++ v3_msg_max_size_tlv(), ++ v3_msg_flags_tlv(), ++ v3_sec_model_tlv()])) ++ end), ++ ++ ?IPRINT("a msgMaxSize below 484 is rejected"), ++ expect_exit(msg_max_size, {bad_msg_max_size, 100}, ++ fun() -> ++ snmp_pdus:dec_message_only( ++ v3_msg([int_tlv(1, 1), ++ int_tlv(100, 1), ++ v3_msg_flags_tlv(), ++ v3_sec_model_tlv()])) ++ end), ++ ++ ?IPRINT("a negative msgSecurityModel is rejected"), ++ expect_exit(msg_sec_model, {bad_msg_sec_model, -1}, ++ fun() -> ++ snmp_pdus:dec_message_only( ++ v3_msg([int_tlv(1, 1), ++ v3_msg_max_size_tlv(), ++ v3_msg_flags_tlv(), ++ raw_int_tlv([255])])) ++ end), ++ ++ ?IPRINT("done"), ++ ok. ++ ++ ++dec_usm_sec_params_int_size_limits(suite) -> []; ++dec_usm_sec_params_int_size_limits(doc) -> ++ ["Content octet limits for msgAuthoritativeEngineBoots and " ++ "msgAuthoritativeEngineTime"]; ++dec_usm_sec_params_int_size_limits(Config) when is_list(Config) -> ++ %% RFC 3414: both fields are INTEGER (0..2147483647) -- four content ++ %% octets. They are parsed before the message is authenticated, so an ++ %% unbounded encoding here is reachable pre-auth. ++ ?IPRINT("well formed security parameters decode"), ++ #usmSecurityParameters{msgAuthoritativeEngineID = "", ++ msgAuthoritativeEngineBoots = 1, ++ msgAuthoritativeEngineTime = 2} = ++ snmp_pdus:dec_usm_security_parameters(usm_params(int_tlv(1, 4), ++ int_tlv(2, 4))), ++ ++ ?IPRINT("msgAuthoritativeEngineBoots with 5 content octets is rejected"), ++ expect_exit(engine_boots, {bad_engine_boots_bytesize, {5, 4}}, ++ fun() -> ++ snmp_pdus:dec_usm_security_parameters( ++ usm_params(int_tlv(1, 5), int_tlv(2, 1))) ++ end), ++ ++ ?IPRINT("msgAuthoritativeEngineTime with 5 content octets is rejected"), ++ expect_exit(engine_time, {bad_engine_time_bytesize, {5, 4}}, ++ fun() -> ++ snmp_pdus:dec_usm_security_parameters( ++ usm_params(int_tlv(1, 1), int_tlv(2, 5))) ++ end), ++ ++ ?IPRINT("done"), ++ ok. ++ ++ ++%%====================================================================== + %% Internal functions + %%====================================================================== + + dec_message(B) when is_binary(B) -> + L = binary_to_list(B), + snmp_pdus:dec_message(L). ++ ++ ++%% ----- ++%% Exception helpers ++%% ++%% dec_class/1 reports the exception class alongside the reason, so that a ++%% throw cannot pass for an exit. ++ ++dec_class(Fun) -> ++ try Fun() of ++ Result -> ++ {ok, Result} ++ catch ++ Class:Reason -> ++ {Class, Reason} ++ end. ++ ++expect_exit(Field, Reason, Fun) -> ++ case dec_class(Fun) of ++ {exit, Reason} -> ++ ok; ++ Unexpected -> ++ exit({unexpected_decode_result, Field, Reason, Unexpected}) ++ end. ++ ++expect_bad_integer(Field, SizeAndLimit, Fun) -> ++ expect_exit(Field, {error, {bad_integer, SizeAndLimit}}, Fun). ++ ++ ++%% ----- ++%% BER encoding helpers ++%% ++%% All lengths stay below 128, so a single octet length is always enough. ++ ++%% INTEGER TLV carrying Value in exactly Size content octets, zero padded. ++%% Padding is what the content octet limits exist to bound, so these ++%% encodings are intentionally non-minimal. ++int_tlv(Value, Size) when is_integer(Value), Value >= 0, Value < 128, ++ is_integer(Size), Size >= 1 -> ++ raw_int_tlv(lists:duplicate(Size - 1, 0) ++ [Value]). ++ ++raw_int_tlv(Octets) -> ++ [2, length(Octets) | Octets]. ++ ++%% TLV for one of the integer valued application types: Size content ++%% octets encoding the value 1. ++padded_value(Tag, Size) when is_integer(Size), Size >= 1 -> ++ [Tag, Size | lists:duplicate(Size - 1, 0) ++ [1]]. ++ ++%% Minimal v3 message for dec_message_only/1: version, a header data ++%% sequence built from the supplied TLVs, and empty msgSecurityParameters. ++v3_msg(HdrDataTLVs) -> ++ HdrData = lists:append(HdrDataTLVs), ++ HdrSeq = [48, length(HdrData) | HdrData], ++ Content = raw_int_tlv([3]) ++ HdrSeq ++ [4, 0], ++ [48, length(Content) | Content]. ++ ++v3_msg_max_size_tlv() -> ++ raw_int_tlv([1, 228]). % 484, the RFC 3412 minimum ++ ++v3_msg_flags_tlv() -> ++ [4, 1, 4]. % reportable, noAuthNoPriv ++ ++v3_sec_model_tlv() -> ++ raw_int_tlv([3]). % USM ++ ++%% USM security parameters with an empty engine ID, user name, and ++%% authentication and privacy parameters. ++usm_params(BootsTLV, TimeTLV) -> ++ Content = [4, 0] ++ BootsTLV ++ TimeTLV ++ [4, 0] ++ [4, 0] ++ [4, 0], ++ [48, length(Content) | Content]. ++ ++%% PDU with the three supplied integer TLVs and an empty varbind list. ++pdu(Tag, IntTLVs) -> ++ Content = lists:append(IntTLVs) ++ [48, 0], ++ [Tag, length(Content) | Content]. ++ ++%% TrapPDU with enterprise 1.3, agent-addr 127.0.0.1, time-stamp 0 and an ++%% empty varbind list. ++trap_pdu(GenericTLV, SpecificTLV) -> ++ Content = [6, 1, 43] ++ [64, 4, 127, 0, 0, 1] ++ ++ GenericTLV ++ SpecificTLV ++ [67, 1, 0] ++ [48, 0], ++ [164, length(Content) | Content]. diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70405.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70405.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70405.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70405.patch 2026-10-19 10:45:27.000000000 +0000 @@ -0,0 +1,81 @@ +From: Konrad Pietrzak +Date: Thu, 25 Jun 2026 13:51:22 +0200 +Subject: inets,stdlib,eldap: Bound integer parsing to mitigate DoS + +inets (httpc): +- Add per-request max_header_size and max_body_size options +- Enforce cumulative header byte count during parsing; reject with + {error, {header_too_long, {max, MaxSize}}} when exceeded +- Default max_header_size is 10240 (same as httpd) +- Validate Content-Length contains only digits before storing, + preventing badarg crashes on malformed responses +- Update limits from new request options on connection reuse +- Catch validation throws in handler, return clean errors to caller +- Document new options in request/4,5 docstring + +stdlib (uri_string): +- Bound port component digit count in uri_string:parse/1 + +eldap: +- Bound port digit count in referral URL parsing + +snmp: +- Bound INTEGER value to max 32-bit value as in SMI-MIB +- Bound Counter32/Gauge32/TimeTicks to 5 bytes (32-bit + sign) +- Bound Counter64 to 9 bytes (64-bit + sign) + +Mitigates GHSA-8qw4-2chm-mvj2. + +Origin: upstream, https://github.com/erlang/otp/commit/aba0fe8c2d700bf4ac94607cf7f00e53bbe4042d +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/snmp/src/misc/snmp_pdus.erl | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +--- a/lib/snmp/src/misc/snmp_pdus.erl ++++ b/lib/snmp/src/misc/snmp_pdus.erl +@@ -378,7 +378,7 @@ dec_value([5,0 | T]) -> + + %% INTEGER + dec_value([2 | Bytes]) -> +- {Value, Rest} = dec_integer_notag(Bytes), ++ {Value, Rest} = dec_integer_notag(Bytes, 4), %% SMI INTEGER is 32-bit + {{'INTEGER', Value}, Rest}; + + %% OCTET STRING +@@ -395,7 +395,7 @@ dec_value([64 | Bytes]) -> + dec_value([65 | Bytes]) -> + %% Counter32 is an unsigned 32 but is actually encoded as + %% a signed integer 32 (INTEGER). +- {Value, Rest} = dec_integer_notag(Bytes), ++ {Value, Rest} = dec_integer_notag(Bytes, 5), + Value2 = + if + (Value >= 0) andalso (Value =< 16#ffffffff) -> +@@ -411,7 +411,7 @@ dec_value([65 | Bytes]) -> + + %% Unsigned32 + dec_value([66 | Bytes]) -> +- {Value, Rest} = dec_integer_notag(Bytes), ++ {Value, Rest} = dec_integer_notag(Bytes, 5), + Value2 = + if + (Value >= 0) andalso (Value =< 16#ffffffff) -> +@@ -425,7 +425,7 @@ dec_value([66 | Bytes]) -> + + %% TimeTicks + dec_value([67 | Bytes]) -> +- {Value, Rest} = dec_integer_notag(Bytes), ++ {Value, Rest} = dec_integer_notag(Bytes, 5), + Value2 = + if + (Value >= 0) andalso (Value =< 16#ffffffff) -> +@@ -446,7 +446,7 @@ dec_value([68 | Bytes]) -> + dec_value([70 | Bytes]) -> + %% Counter64 is an unsigned 64 but is actually encoded as + %% a signed integer 64. +- {Value, Rest} = dec_integer_notag(Bytes), ++ {Value, Rest} = dec_integer_notag(Bytes, 9), + Value2 = + if + (Value >= 0) andalso (Value < 16#8000000000000000) -> diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70409.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70409.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70409.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-70409.patch 2026-10-19 10:45:38.000000000 +0000 @@ -0,0 +1,51 @@ +From: Konrad Pietrzak +Date: Thu, 25 Jun 2026 13:51:22 +0200 +Subject: inets,stdlib,eldap: Bound integer parsing to mitigate DoS + +inets (httpc): +- Add per-request max_header_size and max_body_size options +- Enforce cumulative header byte count during parsing; reject with + {error, {header_too_long, {max, MaxSize}}} when exceeded +- Default max_header_size is 10240 (same as httpd) +- Validate Content-Length contains only digits before storing, + preventing badarg crashes on malformed responses +- Update limits from new request options on connection reuse +- Catch validation throws in handler, return clean errors to caller +- Document new options in request/4,5 docstring + +stdlib (uri_string): +- Bound port component digit count in uri_string:parse/1 + +eldap: +- Bound port digit count in referral URL parsing + +snmp: +- Bound INTEGER value to max 32-bit value as in SMI-MIB +- Bound Counter32/Gauge32/TimeTicks to 5 bytes (32-bit + sign) +- Bound Counter64 to 9 bytes (64-bit + sign) + +Mitigates GHSA-8qw4-2chm-mvj2. + +Origin: upstream, https://github.com/erlang/otp/commit/aba0fe8c2d700bf4ac94607cf7f00e53bbe4042d +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/eldap/src/eldap.erl | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +--- a/lib/eldap/src/eldap.erl ++++ b/lib/eldap/src/eldap.erl +@@ -1882,10 +1882,12 @@ parse_hostport(Str) -> + parse_port(Rest,Sport)}} + end. + +-parse_port(Rest,Sport) -> ++parse_port(Rest,Sport) when length(Sport) =< 5 -> + try list_to_integer(Sport) + catch _:_ -> parse_error(parsing_port,Rest) +- end. ++ end; ++parse_port(Rest, _) -> ++ parse_error(parsing_port, Rest). + + parse_host(Rest,Shost) -> + case catch validate_host(Shost) of diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-1.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-1.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-1.patch 2026-10-19 10:45:56.000000000 +0000 @@ -0,0 +1,318 @@ +From: Konrad Pietrzak +Date: Wed, 19 Aug 2026 14:26:37 +0200 +Subject: inets: Adjust keep_alive_timeout effective measurement in httpd + + keep_alive_timeout would check the time between opening a connection, + or right after sending a response, until the next request's headers + are parsed. + Now the timeout is cancelled as soon as new data arrives, before the + parsing. + Allow atom `infinity` as a value for keep_alive_timeout to disable the + timeout entirely. + +Origin: upstream, https://github.com/erlang/otp/commit/d9500b1fd1139d65a51de079dbeaf86194a3effd +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/inets/src/http_server/httpd_conf.erl | 20 ++- + .../src/http_server/httpd_request_handler.erl | 30 ++-- + lib/inets/test/httpd_SUITE.erl | 146 +++++++++++++++++++- + 3 files changed, 180 insertions(+), 16 deletions(-) + +--- a/lib/inets/src/http_server/httpd_conf.erl ++++ b/lib/inets/src/http_server/httpd_conf.erl +@@ -100,9 +100,22 @@ validate_properties(RawProperties) -> + + %% This function is used to validate inter-property dependencies. + %% That is, if property A depends on property B. +-%% The only sunch preperty at this time is bind_address that depends +-%% on ipfamily. ++%% The only such preperties at this time are: ++%% - bind_address that depends on ipfamily ++%% - keep_alive_timeout that depends on keep_alive + validate_properties2(Properties0) -> ++ case proplists:get_value(keep_alive, Properties0) of ++ undefined -> ok; %% defaults to true ++ true -> ok; ++ false -> ++ KeepaliveTimeout = proplists:get_value(keep_alive_timeout, Properties0), ++ case KeepaliveTimeout of ++ infinity -> ok; %%% timeout disabled ++ undefined -> ok; ++ _ -> ++ throw({error, invalid_keep_alive_timeout}) ++ end ++ end, + Properties = fix_ipfamily(Properties0), + case proplists:get_value(bind_address, Properties) of + undefined -> +@@ -261,7 +274,8 @@ validate_config_params([{max_keep_alive_request, Value} | _]) -> + throw({max_keep_alive_request, Value}); + + validate_config_params([{keep_alive_timeout, Value} | Rest]) +- when is_integer(Value) andalso (Value >= 0) -> ++ when is_integer(Value) andalso (Value >= 0); ++ Value =:= infinity -> + validate_config_params(Rest); + validate_config_params([{keep_alive_timeout, Value} | _]) -> + throw({keep_alive_timeout, Value}); +--- a/lib/inets/src/http_server/httpd_request_handler.erl ++++ b/lib/inets/src/http_server/httpd_request_handler.erl +@@ -115,9 +115,12 @@ init([Manager, ConfigDB, AcceptTimeout]) -> + Peername = http_transport:peername(SocketType, Socket), + Sockname = http_transport:sockname(SocketType, Socket), + ++ Keepalive = httpd_util:lookup(ConfigDB, keep_alive, true), + %%Timeout value is in seconds we want it in milliseconds +- KeepAliveTimeOut = 1000 * httpd_util:lookup(ConfigDB, keep_alive_timeout, 150), +- ++ KeepAliveTimeOut = case Keepalive of ++ true -> 1000 * httpd_util:lookup(ConfigDB, keep_alive_timeout, 150); ++ _ -> infinity ++ end, + case http_transport:negotiate(SocketType, Socket, ?HANDSHAKE_TIMEOUT) of + {error, {tls_alert, {_, AlertDesc}} = Error} -> + ModData = #mod{config_db = ConfigDB, init_data = #init_data{peername = Peername, +@@ -186,7 +189,7 @@ continue_init(Manager, ConfigDB, SocketType, Socket, Peername, Sockname, + chunk = chunk_start(MaxChunk)}, + setopts(Socket, SocketType, [binary, {packet, 0}, {active, once}]), + NewState = +- data_receive_counter(activate_request_timeout(State), ++ data_receive_counter(activate_keepalive_timeout(State), + httpd_util:lookup(ConfigDB, minimum_bytes_per_second, false)), + gen_server:enter_loop(?MODULE, [], NewState) + end. +@@ -237,7 +240,7 @@ handle_info({Proto, Socket, Data}, + when (((Proto =:= tcp) orelse + (Proto =:= ssl) orelse + (Proto =:= dummy)) andalso is_binary(Data)) -> +- ++ cancel_keepalive_timeout(State), + NewDataSize = case State#state.byte_limit of + undefined -> + undefined; +@@ -249,9 +252,9 @@ handle_info({Proto, Socket, Data}, + {ok, Result} -> + NewState = case NewDataSize of + undefined -> +- cancel_request_timeout(State); ++ State; + _ -> +- set_new_data_size(cancel_request_timeout(State), NewDataSize) ++ set_new_data_size(State, NewDataSize) + end, + handle_msg(Result, NewState); + +@@ -357,7 +360,7 @@ terminate(_Reason, State) -> + do_terminate(State). + + do_terminate(#state{mod = ModData} = State) -> +- cancel_request_timeout(State), ++ cancel_keepalive_timeout(State), + httpd_socket:close(ModData#mod.socket_type, ModData#mod.socket). + + format_status(normal, [_, State]) -> +@@ -721,7 +724,7 @@ handle_next_request(#state{mod = #mod{connection = true} = ModData, + chunk = chunk_start(MaxChunk), + response_sent = false}, + +- NewState = activate_request_timeout(TmpState), ++ NewState = activate_keepalive_timeout(TmpState), + + case Data of + <<>> -> +@@ -734,9 +737,12 @@ handle_next_request(#state{mod = #mod{connection = true} = ModData, + handle_next_request(State, _) -> + {stop, normal, State}. + +-activate_request_timeout(#state{timeout = Time} = State) -> ++activate_keepalive_timeout(#state{timeout = infinity} = State) -> ++ State; ++activate_keepalive_timeout(#state{timeout = Time} = State) -> ++ NewState = cancel_keepalive_timeout(State), + Ref = erlang:send_after(Time, self(), timeout), +- State#state{timer = Ref}. ++ NewState#state{timer = Ref}. + data_receive_counter(State, Byte_limit) -> + case Byte_limit of + false -> +@@ -745,9 +751,9 @@ data_receive_counter(State, Byte_limit) -> + erlang:send_after(3000, self(), check_data_first), + State#state{data = 0, byte_limit = Nr} + end. +-cancel_request_timeout(#state{timer = undefined} = State) -> ++cancel_keepalive_timeout(#state{timer = undefined} = State) -> + State; +-cancel_request_timeout(#state{timer = Timer} = State) -> ++cancel_keepalive_timeout(#state{timer = Timer} = State) -> + erlang:cancel_timer(Timer), + receive + timeout -> +--- a/lib/inets/test/httpd_SUITE.erl ++++ b/lib/inets/test/httpd_SUITE.erl +@@ -85,7 +85,9 @@ all() -> + {group, https_alert}, + {group, https_not_sup}, + {group, esi}, +- mime_types_format ++ mime_types_format, ++ keepalive_timeout, ++ keepalive_timeout_disabled + ]. + + groups() -> +@@ -404,6 +406,9 @@ init_per_testcase(disk_log_bad_file, Config0) -> + init_per_testcase(erl_script_timeout_default, Config) -> + ct:timetrap({seconds, 60}), + dbg(erl_script_timeout_default, Config, init); ++init_per_testcase(Case, Config) when Case == keepalive_timeout; ++ Case == keepalive_timeout_disabled -> ++ dbg(Case, Config, init); + init_per_testcase(medium = Case, Config) -> + ct:timetrap({seconds, 150}), + dbg(Case, Config, init); +@@ -2261,6 +2266,145 @@ mime_types_format(Config) when is_list(Config) -> + {"cpt","application/mac-compactpro"}, + {"hqx","application/mac-binhex40"}]} = httpd_conf:load_mime_types(MimeTypes). + ++%%------------------------------------------------------------------------- ++keepalive_timeout() -> ++ [{doc, "A client that stalls the connection must be timed out (Slowloris)."}, ++ {timetrap, {seconds, 30}}]. ++keepalive_timeout(Config) when is_list(Config) -> ++ inets_test_lib:start_apps([inets]), ++ PrivDir = proplists:get_value(priv_dir, Config), ++ DocRoot = filename:join(PrivDir, "keepalive_timeout_htdocs"), ++ ok = file:make_dir(DocRoot), ++ ok = file:write_file(filename:join(DocRoot, "index.html"), <<"OK">>), ++ KeepAliveTimeout = 2, ++ {ok, Pid} = inets:start(httpd, [ ++ {port, 0}, ++ {server_name, "body_timeout_test"}, ++ {server_root, PrivDir}, ++ {document_root, DocRoot}, ++ {bind_address, {127,0,0,1}}, ++ {keep_alive_timeout, KeepAliveTimeout}, ++ {minimum_bytes_per_second, false}, ++ {modules, [mod_get]} ++ ]), ++ Info = httpd:info(Pid), ++ {port, Port} = lists:keyfind(port, 1, Info), ++ T1 = os:timestamp(), ++ %% Open the connection and stall ++ {ok, Sock1} = gen_tcp:connect("127.0.0.1", Port, [binary, {active, false}]), ++ ++ %% Server should timeout and respond with 408 within ~2 seconds ++ {error, closed} = gen_tcp:recv(Sock1, 0, 5000), ++ T2 = os:timestamp(), ++ case timer:now_diff(T2, T1)/1_000_000 of ++ T when T < KeepAliveTimeout * 0.9 orelse T > KeepAliveTimeout * 1.1 -> ++ ct:fail("Timeout outside of allowed error"); ++ _ -> ok ++ end, ++ ++ %% Open the connection, send a valid request, then stall ++ {ok, Sock2} = gen_tcp:connect("127.0.0.1", Port, [binary, {active, true}]), ++ ok = gen_tcp:send(Sock2, <<"GET /index.html HTTP/1.1\r\nHost: localhost\r\n" ++ "Content-Length: 1\r\n\r\nX">>), ++ %% receive the body first, then flush the rest ++ receive ++ {tcp, _, Bin1} when binary_part(Bin1, {byte_size(Bin1), -2}) =:= <<"OK">> -> ok ++ after KeepAliveTimeout * 1000 -> ++ ct:fail("Response body not received within the timeout") ++ end, ++ inets_test_lib:flush(), ++ ++ timer:sleep(KeepAliveTimeout * 1000), ++ {error, closed} = gen_tcp:send(Sock2, <<"GET /index.html HTTP/1.1\r\nHost: localhost\r\n" ++ "Content-Length: 1\r\n\r\nX">>), ++ ++ %% Connect, and send one byte, then stall over the timeout value, then continue. ++ %% First byte should cancel the timeout ++ %% Timeout should be reactivated afterwards ++ {ok, Sock3} = gen_tcp:connect("127.0.0.1", Port, [binary, {active, true}]), ++ ok = gen_tcp:send(Sock3, <<"G">>), ++ timer:sleep(KeepAliveTimeout*2), ++ ok = gen_tcp:send(Sock3, <<"ET /index.html HTTP/1.1\r\nHost: localhost\r\n" ++ "Content-Length: 1\r\n\r\nX">>), ++ receive ++ {tcp, _, Bin2} when binary_part(Bin2, {byte_size(Bin2), -2}) =:= <<"OK">> -> ok ++ after KeepAliveTimeout * 1000 -> ++ ct:fail("Response body not received within the timeout") ++ end, ++ inets_test_lib:flush(), ++ timer:sleep(KeepAliveTimeout * 1000), ++ {error, closed} = gen_tcp:send(Sock3, <<"GET /index.html HTTP/1.1\r\nHost: localhost\r\n" ++ "Content-Length: 1\r\n\r\nX">>), ++ inets:stop(httpd, Pid), ++ inets_test_lib:stop_apps([inets]). ++ ++%%------------------------------------------------------------------------- ++keepalive_timeout_disabled() -> ++ [{doc, "keep_alive_timeout must not be applied when {keep_alive, false} " ++ "is configured: a client that pauses between the headers and the " ++ "body for longer than keep_alive_timeout must still get its " ++ "request served, not a 408 response."}, ++ {timetrap, {seconds, 180}}]. ++keepalive_timeout_disabled(Config) when is_list(Config) -> ++ inets_test_lib:start_apps([inets]), ++ PrivDir = proplists:get_value(priv_dir, Config), ++ DocRoot = filename:join(PrivDir, "keepalive_disabled_htdocs"), ++ ++ ok = file:make_dir(DocRoot), ++ ok = file:write_file(filename:join(DocRoot, "index.html"), <<"OK">>), ++ %% First test interoptions dependency ++ {error, invalid_keep_alive_timeout} = inets:start(httpd, [ ++ {port, 0}, ++ {server_name, "keepalive_disabled_test"}, ++ {server_root, PrivDir}, ++ {document_root, DocRoot}, ++ {bind_address, {127,0,0,1}}, ++ {keep_alive, false}, ++ {keep_alive_timeout, 2}, ++ {minimum_bytes_per_second, false}, ++ {modules, [mod_esi]}, ++ {erl_script_alias, {"/erl", [httpd_example]}} ++ ]), ++ {ok, Pid0} = inets:start(httpd, [ ++ {port, 0}, ++ {server_name, "keepalive_disabled_test"}, ++ {server_root, PrivDir}, ++ {document_root, DocRoot}, ++ {bind_address, {127,0,0,1}}, ++ {keep_alive_timeout, 2}, %%% keep_alive is true by default ++ {minimum_bytes_per_second, false}, ++ {modules, [mod_esi]}, ++ {erl_script_alias, {"/erl", [httpd_example]}} ++ ]), ++ inets:stop(httpd, Pid0), ++ ++ {ok, Pid} = inets:start(httpd, [ ++ {port, 0}, ++ {server_name, "keepalive_disabled_test"}, ++ {server_root, PrivDir}, ++ {document_root, DocRoot}, ++ {bind_address, {127,0,0,1}}, ++ {keep_alive, false}, ++ {minimum_bytes_per_second, false}, ++ {modules, [mod_get]} ++ ]), ++ Info = httpd:info(Pid), ++ {port, Port} = lists:keyfind(port, 1, Info), ++ ++ {ok, Sock} = gen_tcp:connect("127.0.0.1", Port, [binary, {active, true}]), ++ %% Send the request after the default value of keep_alive_timeout ++ ct:sleep({seconds, 150}), ++ ++ ok = gen_tcp:send(Sock, <<"GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n">>), ++ % {ok, Response} = gen_tcp:recv(Sock, 0, 5000), ++ receive ++ {tcp, _, Bin2} when binary_part(Bin2, {byte_size(Bin2), -2}) =:= <<"OK">> -> ok ++ after 10_000 -> ++ ct:fail("Response body not received") ++ end. ++ ++%%------------------------------------------------------------------------- ++ + erl_script_timeout_default(Config) when is_list(Config) -> + ServerConfig = [ + {modules, [mod_esi]}, diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-2.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-2.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-2.patch 2026-10-19 10:46:15.000000000 +0000 @@ -0,0 +1,431 @@ +From: Lukas Backström +Date: Wed, 12 Aug 2026 12:52:18 +0200 +Subject: inets: Add httpd option max_body_read_timeout + +This option sets a max timeout for how long a body read will wait +for there to be more data delivered before it is terminated. + +A similar limit can be set by using max_bytes_per_second, but that +is much harder to tune and predict, so we add a new option that +reflects how nginx handles such timeouts. + +Origin: upstream, https://github.com/erlang/otp/commit/f46293c977608e5656aaa2a78a5492faccd0fe09 +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/inets/src/http_lib/http_internal.hrl | 3 +- + lib/inets/src/http_server/httpd.erl | 18 +++- + lib/inets/src/http_server/httpd_conf.erl | 6 ++ + .../src/http_server/httpd_request_handler.erl | 93 +++++++++++--------- + lib/inets/test/httpd_basic_SUITE.erl | 93 +++++++++++++++++++- + 5 files changed, 165 insertions(+), 48 deletions(-) + +--- a/lib/inets/src/http_lib/http_internal.hrl ++++ b/lib/inets/src/http_lib/http_internal.hrl +@@ -29,7 +29,8 @@ + -define(HTTP_MAX_URI_SIZE, nolimit). + -define(HTTP_MAX_VERSION_STRING, 8). + -define(HTTP_MAX_METHOD_STRING, 20). +--define(HTTP_MAX_CONTENT_LENGTH, 100000000). ++-define(HTTP_MAX_CONTENT_LENGTH, 100000000). %% 100 MB ++-define(HTTP_MAX_BODY_READ_TIMEOUT, 60). %% seconds + -define(HTTP_MAX_CLIENTS, 150). + + %%% Response headers +--- a/lib/inets/src/http_server/httpd.erl ++++ b/lib/inets/src/http_server/httpd.erl +@@ -167,11 +167,17 @@ property list. + + - [](){: #prop_keep_alive_timeout } **`{keep_alive_timeout, integer()}`** + The number of seconds the server waits for a subsequent request from the +- client before closing the connection. Default is `150`. ++ client before closing the connection. This timer covers both the waiting time ++ for the next request to arrive and the parsing of the headers of that request. ++ Default is `150` seconds. + +-- [](){: #prop_max_body_size } **`{max_body_size, integer()}`** ++- [](){: #prop_max_body_size } **`{max_body_size, integer() | nolimit}`** + Limits the size of the message body of an HTTP request. Default is no limit. + ++- [](){: #prop_max_body_read_timeout } **`{max_body_read_timeout, pos_integer()}`** ++ Limits the time allowed for the server to be idle between message body reads ++ of an HTTP request. Default is `60` seconds. ++ + - [](){: #prop_max_clients } **`{max_clients, integer()}`** + Limits the number of simultaneous requests that can be supported. Default is + `150`. +@@ -184,7 +190,7 @@ property list. + larger than this are answered with status 413. Default is `100000000` (100 + MB). + +-- [](){: #prop_max_uri } **`{max_uri_size, integer()}`** ++- [](){: #prop_max_uri } **`{max_uri_size, integer() | nolimit}`** + Limits the size of the HTTP request URI. Default is no limit. + + - [](){: #prop_max_keep_alive_req } **`{max_keep_alive_request, integer()}`** +@@ -989,6 +995,7 @@ reload_config(ConfigFile, Mode) -> + | {keep_alive, boolean()} + | {keep_alive_timeout, integer()} + | {max_body_size, integer()} ++ | {max_body_read_timeout, pos_integer()} + | {max_clients, integer()} + | {max_header_size, integer()} + | {max_content_length, integer()} +@@ -1041,6 +1048,7 @@ server. + | {keep_alive, boolean()} + | {keep_alive_timeout, integer()} + | {max_body_size, integer()} ++ | {max_body_read_timeout, pos_integer()} + | {max_clients, integer()} + | {max_header_size, integer()} + | {max_content_length, integer()} +@@ -1079,6 +1087,7 @@ server. + | {keep_alive, boolean()} + | {keep_alive_timeout, integer()} + | {max_body_size, integer()} ++ | {max_body_read_timeout, pos_integer()} + | {max_clients, integer()} + | {max_header_size, integer()} + | {max_content_length, integer()} +@@ -1134,6 +1143,7 @@ info(Address, Port) when is_integer(Port) -> + | {keep_alive, boolean()} + | {keep_alive_timeout, integer()} + | {max_body_size, integer()} ++ | {max_body_read_timeout, pos_integer()} + | {max_clients, integer()} + | {max_header_size, integer()} + | {max_content_length, integer()} +@@ -1173,6 +1183,7 @@ info(Address, Port) when is_integer(Port) -> + | {keep_alive, boolean()} + | {keep_alive_timeout, integer()} + | {max_body_size, integer()} ++ | {max_body_read_timeout, pos_integer()} + | {max_clients, integer()} + | {max_header_size, integer()} + | {max_content_length, integer()} +@@ -1231,6 +1242,7 @@ options of the server. + | {keep_alive, boolean()} + | {keep_alive_timeout, integer()} + | {max_body_size, integer()} ++ | {max_body_read_timeout, pos_integer()} + | {max_clients, integer()} + | {max_header_size, integer()} + | {max_content_length, integer()} +--- a/lib/inets/src/http_server/httpd_conf.erl ++++ b/lib/inets/src/http_server/httpd_conf.erl +@@ -201,6 +201,12 @@ validate_config_params([{max_body_size, Value} | Rest]) + validate_config_params([{max_body_size, Value} | _]) -> + throw({max_body_size, Value}); + ++validate_config_params([{max_body_read_timeout, Value} | Rest]) ++ when is_integer(Value) andalso (Value > 0) -> ++ validate_config_params(Rest); ++validate_config_params([{max_body_read_timeout, Value} | _]) -> ++ throw({max_body_read_timeout, Value}); ++ + validate_config_params([{max_content_length, Value} | Rest]) + when is_integer(Value) andalso (Value > 0) -> + validate_config_params(Rest); +--- a/lib/inets/src/http_server/httpd_request_handler.erl ++++ b/lib/inets/src/http_server/httpd_request_handler.erl +@@ -45,8 +45,9 @@ + mfa, %% {Module, Function, Args} + max_keep_alive_request = infinity, %% integer() | infinity + response_sent = false :: boolean(), +- timeout, %% infinity | integer() > 0 +- timer :: 'undefined' | reference(), % Request timer ++ timeout :: pos_integer(), %% ms ++ body_timeout :: pos_integer(), %% ms ++ timer :: 'undefined' | {reference(), request_timeout | body_timeout}, %% Request/body timer + headers = #http_request_h{}, + body, %% binary() + data, %% The total data received in bits, checked after 10s +@@ -157,6 +158,7 @@ continue_init(Manager, ConfigDB, SocketType, Socket, Peername, Sockname, + MaxURISize = max_uri_size(ConfigDB), + NrOfRequest = max_keep_alive_request(ConfigDB), + MaxContentLen = max_content_length(ConfigDB), ++ MaxBodyTimeout = max_body_read_timeout(ConfigDB), + Customize = customize(ConfigDB), + MaxChunk = max_client_body_chunk(ConfigDB), + +@@ -184,9 +186,10 @@ continue_init(Manager, ConfigDB, SocketType, Socket, Peername, Sockname, + manager = Manager, + status = Status, + timeout = TimeOut, ++ body_timeout = MaxBodyTimeout, + max_keep_alive_request = NrOfRequest, + mfa = MFA, +- chunk = chunk_start(MaxChunk)}, ++ chunk = chunk_start(MaxChunk)}, + setopts(Socket, SocketType, [binary, {packet, 0}, {active, once}]), + NewState = + data_receive_counter(activate_keepalive_timeout(State), +@@ -240,22 +243,12 @@ handle_info({Proto, Socket, Data}, + when (((Proto =:= tcp) orelse + (Proto =:= ssl) orelse + (Proto =:= dummy)) andalso is_binary(Data)) -> +- cancel_keepalive_timeout(State), +- NewDataSize = case State#state.byte_limit of +- undefined -> +- undefined; +- _ -> +- State#state.data + byte_size(Data) +- end, ++ cancel_timeout(State), + + try Module:Function([Data | Args]) of + {ok, Result} -> +- NewState = case NewDataSize of +- undefined -> +- State; +- _ -> +- set_new_data_size(State, NewDataSize) +- end, ++ BodyTimeoutState = activate_body_timeout(State), ++ NewState = update_data_size(BodyTimeoutState, Data), + handle_msg(Result, NewState); + + %% error returns from httpd_request +@@ -282,12 +275,9 @@ handle_info({Proto, Socket, Data}, + + {_M, _F, _A} = NewMFA -> + setopts(Socket, SockType, [{active, once}]), +- case NewDataSize of +- undefined -> +- {noreply, State#state{mfa = NewMFA}}; +- _ -> +- {noreply, State#state{mfa = NewMFA, data = NewDataSize}} +- end ++ BodyTimeoutState = reset_body_timeout(State, Data), ++ NewState = update_data_size(BodyTimeoutState, Data), ++ {noreply, NewState#state{mfa = NewMFA}} + catch throw:{error, Error} when Module =:= http_chunk -> + handle_chunk_size_error(Error, State) + end; +@@ -360,7 +350,7 @@ terminate(_Reason, State) -> + do_terminate(State). + + do_terminate(#state{mod = ModData} = State) -> +- cancel_keepalive_timeout(State), ++ cancel_timeout(State), + httpd_socket:close(ModData#mod.socket_type, ModData#mod.socket). + + format_status(normal, [_, State]) -> +@@ -388,8 +378,11 @@ code_change(_OldVsn, State, _Extra) -> + %%-------------------------------------------------------------------- + %%% Internal functions + %%-------------------------------------------------------------------- +-set_new_data_size(State, NewData) -> +- State#state{data = NewData}. ++update_data_size(State = #state{ byte_limit = undefined}, _) -> ++ State; ++update_data_size(State, Data) -> ++ State#state{data = State#state.data + byte_size(Data)}. ++ + await_socket_ownership_transfer(AcceptTimeout) -> + receive + {socket_ownership_transfered, SocketType, Socket} -> +@@ -560,8 +553,7 @@ handle_body(#state{headers = Headers, body = Body, + %% Whole body delivered, if chunking mechanism is enabled the whole + %% body fits in one chunk. + {ok, NewBody} when is_binary(NewBody) -> +- handle_response(State#state{chunk = chunk_finish(ChunkState, +- CbState, MaxChunk), ++ handle_response(State#state{chunk = chunk_finish(ChunkState, CbState, MaxChunk), + headers = Headers, + body = NewBody}) + end; +@@ -671,6 +663,8 @@ handle_internal_chunk(#state{chunk = {ChunkState, CbState}, body = Chunk, + mfa = {Module, Function, Args}}) + end. + ++handle_response(#state{timer = {_Ref, _}} = State) -> ++ handle_response(cancel_timeout(State)); + handle_response(#state{body = Body, + headers = Headers, + mod = ModData, +@@ -739,10 +733,37 @@ handle_next_request(State, _) -> + + activate_keepalive_timeout(#state{timeout = infinity} = State) -> + State; ++activate_keepalive_timeout(#state{timer = {_Ref, keep_alive_timeout}} = State) -> ++ activate_keepalive_timeout(cancel_timeout(State)); + activate_keepalive_timeout(#state{timeout = Time} = State) -> +- NewState = cancel_keepalive_timeout(State), + Ref = erlang:send_after(Time, self(), timeout), +- NewState#state{timer = Ref}. ++ State#state{timer = {Ref, keep_alive_timeout}}. ++ ++activate_body_timeout(#state{timer = {_Ref, body_timeout}} = State) -> ++ activate_body_timeout(cancel_timeout(State)); ++activate_body_timeout(#state{timer = undefined, body_timeout = Time} = State) -> ++ Ref = erlang:send_after(Time, self(), timeout), ++ State#state{timer = {Ref, body_timeout}}; ++activate_body_timeout(State) -> ++ State. ++ ++reset_body_timeout(#state{timer = {_Ref, body_timeout}} = State, Data) when byte_size(Data) > 0 -> ++ activate_body_timeout(cancel_timeout(State)); ++reset_body_timeout(State, _Data) -> ++ State. ++ ++cancel_timeout(#state{timer = undefined} = State) -> ++ State; ++cancel_timeout(#state{timer = {Timer, _}} = State) -> ++ erlang:cancel_timer(Timer), ++ receive ++ timeout -> ++ ok ++ after 0 -> ++ ok ++ end, ++ State#state{timer = undefined}. ++ + data_receive_counter(State, Byte_limit) -> + case Byte_limit of + false -> +@@ -751,17 +772,6 @@ data_receive_counter(State, Byte_limit) -> + erlang:send_after(3000, self(), check_data_first), + State#state{data = 0, byte_limit = Nr} + end. +-cancel_keepalive_timeout(#state{timer = undefined} = State) -> +- State; +-cancel_keepalive_timeout(#state{timer = Timer} = State) -> +- erlang:cancel_timer(Timer), +- receive +- timeout -> +- ok +- after 0 -> +- ok +- end, +- State#state{timer = undefined}. + + decrease(N) when is_integer(N) -> + N-1; +@@ -790,6 +800,9 @@ max_keep_alive_request(ConfigDB) -> + max_content_length(ConfigDB) -> + httpd_util:lookup(ConfigDB, max_content_length, ?HTTP_MAX_CONTENT_LENGTH). + ++max_body_read_timeout(ConfigDB) -> ++ 1000 * httpd_util:lookup(ConfigDB, max_body_read_timeout, ?HTTP_MAX_BODY_READ_TIMEOUT). ++ + customize(ConfigDB) -> + httpd_util:lookup(ConfigDB, customize, httpd_custom). + +--- a/lib/inets/test/httpd_basic_SUITE.erl ++++ b/lib/inets/test/httpd_basic_SUITE.erl +@@ -46,9 +46,10 @@ groups() -> + escaped_url_in_error_body, + script_timeout, + chunked_body_size_unbounded, +- slowdose, ++ {group, slowdose}, + keep_alive_timeout, +- invalid_rfc1123_date]}]. ++ invalid_rfc1123_date]}, ++ {slowdose, [parallel], [slowdose_min_bytes, slowdose_slow_header, slowdose_slow_body, slowdose_trickle_body]}]. + + init_per_group(_GroupName, Config) -> + Config. +@@ -382,9 +383,9 @@ verify_script_timeout(Config, ScriptTimeout, StatusCode) -> + + %%------------------------------------------------------------------------- + +-slowdose() -> ++slowdose_min_bytes() -> + [{doc, "Testing minimum bytes per second option"}]. +-slowdose(Config) when is_list(Config) -> ++slowdose_min_bytes(Config) when is_list(Config) -> + HttpdConf = proplists:get_value(httpd_conf, Config), + {ok, Pid} = inets:start(httpd, [{port, 0}, {minimum_bytes_per_second, 200}|HttpdConf]), + Info = httpd:info(Pid), +@@ -395,6 +396,90 @@ slowdose(Config) when is_list(Config) -> + {error, closed} = gen_tcp:send(Socket, "Hey") + end. + ++slowdose_slow_header() -> ++ [{doc, "Testing timeout for slow headers"}]. ++slowdose_slow_header(Config) when is_list(Config) -> ++ HttpdConf = proplists:get_value(httpd_conf, Config), ++ {ok, Pid} = inets:start(httpd, [{port, 0}, {keep_alive_timeout, 2} | HttpdConf]), ++ Info = httpd:info(Pid), ++ Port = proplists:get_value(port, Info), ++ {ok, Socket} = inets_test_lib:connect_bin(ip_comm, "localhost", Port, []), ++ ok = inets_test_lib:send(ip_comm, Socket, "GET /dummy.html HTTP/1.0\r\n"), ++ receive ++ {tcp, Socket, Data} -> ++ case binary:match(Data, <<"408">>) of ++ {_, _} -> ++ ok; ++ nomatch -> ++ ct:fail("Server did not reply with 408 timeout") ++ end; ++ Else -> ++ ct:fail({"Unexpected data: ", Else}) ++ after 3000 -> ++ ct:fail("Server did not close connection after slow header") ++ end. ++ ++slowdose_slow_body() -> ++ [{doc, "Testing timeout for slow body"}]. ++slowdose_slow_body(Config) when is_list(Config) -> ++ HttpdConf = proplists:get_value(httpd_conf, Config), ++ {ok, Pid} = inets:start(httpd, [{port, 0}, {max_body_read_timeout, 1} | HttpdConf]), ++ Info = httpd:info(Pid), ++ Port = proplists:get_value(port, Info), ++ {ok, Socket} = inets_test_lib:connect_bin(ip_comm, "localhost", Port, []), ++ ok = inets_test_lib:send(ip_comm, Socket, "POST /dummy.html HTTP/1.1\r\n" ++ ++ "Host: localhost\r\n" ++ ++ "Content-Length: 10\r\n\r\n" ++ ++ "12345"), ++ receive ++ {tcp, Socket, Data} -> ++ case binary:match(Data, <<"408">>) of ++ {_, _} -> ++ ok; ++ nomatch -> ++ ct:fail({"Server did not reply with 408 timeout", Data}) ++ end; ++ Else -> ++ ct:fail({"Unexpected data: ", Else}) ++ after 3000 -> ++ ct:fail("Server did not close connection after slow header") ++ end. ++ ++slowdose_trickle_body() -> ++ [{doc, "Testing timeout for trickling body"}]. ++slowdose_trickle_body(Config) when is_list(Config) -> ++ ++ %% First we test that max_body_read_timeout does not trigger this ++ slowdose_trickle_body(Config, {max_body_read_timeout, 1}, <<"501">>), ++ ++ %% Then we test that minimum_bytes_per_second does trigger it ++ slowdose_trickle_body(Config, {minimum_bytes_per_second, 50}, <<"408">>). ++ ++slowdose_trickle_body(Config, HttpdConfig, Status) -> ++ HttpdConf = proplists:get_value(httpd_conf, Config), ++ {ok, Pid} = inets:start(httpd, [{port, 0}, HttpdConfig | HttpdConf]), ++ Info = httpd:info(Pid), ++ Port = proplists:get_value(port, Info), ++ {ok, Socket} = inets_test_lib:connect_bin(ip_comm, "localhost", Port, []), ++ ok = inets_test_lib:send(ip_comm, Socket, "POST /dummy.html HTTP/1.1\r\n" ++ ++ "Host: localhost\r\n" ++ ++ "Content-Length: 10\r\n\r\n"), ++ [timer:sleep(500), inets_test_lib:send(ip_comm, Socket, integer_to_list(N)) || N <- lists:seq(1, 10)], ++ receive ++ {tcp, Socket, Data} -> ++ case binary:match(Data, Status) of ++ {_, _} -> ++ %% Flush the 501 contents if that is what we got, to avoid it interfering with other tests ++ inets_test_lib:flush(); ++ nomatch -> ++ ct:fail({"Server did not reply with 501 Not Implemented", Data}) ++ end; ++ Else1 -> ++ ct:fail({"Unexpected data: ", Else1}) ++ after 3000 -> ++ ct:fail("Server did not close connection after slow header") ++ end. ++ + %%------------------------------------------------------------------------- + + invalid_rfc1123_date() -> diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-3.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-3.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-3.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-3.patch 2026-09-30 10:13:04.000000000 +0000 @@ -0,0 +1,39 @@ +From: Lukas Backström +Date: Mon, 1 Jun 2026 15:14:49 +0200 +Subject: inets: Send 408 when we hit min_bytes_per_second floor + +Origin: upstream, https://github.com/erlang/otp/commit/3bc8abe9d1e8f767389b1762cbebee651874775d +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/inets/src/http_server/httpd_request_handler.erl | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) + +--- a/lib/inets/src/http_server/httpd_request_handler.erl ++++ b/lib/inets/src/http_server/httpd_request_handler.erl +@@ -301,20 +301,24 @@ handle_info(timeout, #state{mod = ModData} = State) -> + httpd_response:send_status(ModData, 408, "Request timeout", "The client did not send the whole request before the " + "server side timeout"), + {stop, normal, State#state{response_sent = true}}; +-handle_info(check_data_first, #state{data = Data, byte_limit = Byte_Limit} = State) -> ++handle_info(check_data_first, #state{mod = ModData, data = Data, byte_limit = Byte_Limit} = State) -> + case Data >= (Byte_Limit*3) of + true -> + erlang:send_after(1000, self(), check_data), + {noreply, State#state{data = 0}}; + _ -> ++ httpd_response:send_status(ModData, 408, "Request timeout", "The client did not send the whole request before the " ++ "server side timeout"), + {stop, normal, State#state{response_sent = true}} + end; +-handle_info(check_data, #state{data = Data, byte_limit = Byte_Limit} = State) -> ++handle_info(check_data, #state{mod = ModData, data = Data, byte_limit = Byte_Limit} = State) -> + case Data >= Byte_Limit of + true -> + erlang:send_after(1000, self(), check_data), + {noreply, State#state{data = 0}}; + _ -> ++ httpd_response:send_status(ModData, 408, "Request timeout", "The client did not send the whole request before the " ++ "server side timeout"), + {stop, normal, State#state{response_sent = true}} + end; + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-4.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-4.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-4.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-4.patch 2026-10-19 10:46:52.000000000 +0000 @@ -0,0 +1,374 @@ +From: Konrad Pietrzak +Date: Wed, 19 Aug 2026 17:49:02 +0200 +Subject: inets: rename max_body_read_timeout option to request_timeout + + - Rename the max_body_read_timeout option to request_timeout + (default unchanged, 60 seconds), matching its documented purpose + of bounding idle time between message chunk reads. + - Rename HTTP_MAX_BODY_READ_TIMEOUT macro to HTTP_REQUEST_READ_TIMEOUT. + - Rename #state fields timeout/body_timeout to keepalive_timeout/ + request_timeout in httpd_request_handler, and the corresponding + activate_body_timeout/reset_body_timeout functions to + activate_request_timeout/reset_request_timeout. + - Guard activate_keepalive_timeout/1 with timer = undefined so it + only arms a fresh timer, instead of unconditionally overwriting + a running one. + - Update httpd.erl -doc and -spec LimitOption references, and widen + keep_alive_timeout/request_timeout types to include infinity(). + - Update httpd_basic_SUITE to use the renamed request_timeout option. + +Origin: upstream, https://github.com/erlang/otp/commit/81b453aac5a006bb8d26405f2bc3cf24e9d7733c +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/inets/src/http_lib/http_internal.hrl | 2 +- + lib/inets/src/http_server/httpd.erl | 38 ++++++----- + lib/inets/src/http_server/httpd_conf.erl | 11 +-- + .../src/http_server/httpd_request_handler.erl | 71 ++++++++++---------- + lib/inets/test/httpd_basic_SUITE.erl | 18 ++--- + 5 files changed, 73 insertions(+), 67 deletions(-) + +--- a/lib/inets/src/http_lib/http_internal.hrl ++++ b/lib/inets/src/http_lib/http_internal.hrl +@@ -30,7 +30,7 @@ + -define(HTTP_MAX_VERSION_STRING, 8). + -define(HTTP_MAX_METHOD_STRING, 20). + -define(HTTP_MAX_CONTENT_LENGTH, 100000000). %% 100 MB +--define(HTTP_MAX_BODY_READ_TIMEOUT, 60). %% seconds ++-define(HTTP_REQUEST_READ_TIMEOUT, 60). %% seconds + -define(HTTP_MAX_CLIENTS, 150). + + %%% Response headers +--- a/lib/inets/src/http_server/httpd.erl ++++ b/lib/inets/src/http_server/httpd.erl +@@ -165,18 +165,20 @@ property list. + Instructs the server whether to use persistent connections when the client + claims to be HTTP/1.1 compliant. Default is `true`. + +-- [](){: #prop_keep_alive_timeout } **`{keep_alive_timeout, integer()}`** ++- [](){: #prop_keep_alive_timeout } **`{keep_alive_timeout, integer() | infinity}`** + The number of seconds the server waits for a subsequent request from the +- client before closing the connection. This timer covers both the waiting time +- for the next request to arrive and the parsing of the headers of that request. ++ client before closing the connection. This timer covers the waiting time ++ for the request to arrive, starting from the connection opening or the ++ moment the server sent a response to a previous request. + Default is `150` seconds. + + - [](){: #prop_max_body_size } **`{max_body_size, integer() | nolimit}`** + Limits the size of the message body of an HTTP request. Default is no limit. + +-- [](){: #prop_max_body_read_timeout } **`{max_body_read_timeout, pos_integer()}`** +- Limits the time allowed for the server to be idle between message body reads +- of an HTTP request. Default is `60` seconds. ++- [](){: #prop_request_timeout } **`{request_timeout, pos_integer() | infinity}`** ++ Limits the time the server is allowed to be idle waiting for the next chunk ++ of an HTTP request. Refreshes on every incoming chunk within the same request. ++ Default is `60` seconds. + + - [](){: #prop_max_clients } **`{max_clients, integer()}`** + Limits the number of simultaneous requests that can be supported. Default is +@@ -993,9 +995,9 @@ reload_config(ConfigFile, Mode) -> + LimitOption :: {customize, atom()} + | {disable_chunked_transfer_encoding_send, boolean()} + | {keep_alive, boolean()} +- | {keep_alive_timeout, integer()} ++ | {keep_alive_timeout, integer() | infinity} + | {max_body_size, integer()} +- | {max_body_read_timeout, pos_integer()} ++ | {request_timeout, pos_integer() | infinity} + | {max_clients, integer()} + | {max_header_size, integer()} + | {max_content_length, integer()} +@@ -1046,9 +1048,9 @@ server. + LimitOption :: {customize, atom()} + | {disable_chunked_transfer_encoding_send, boolean()} + | {keep_alive, boolean()} +- | {keep_alive_timeout, integer()} ++ | {keep_alive_timeout, integer() | infinity} + | {max_body_size, integer()} +- | {max_body_read_timeout, pos_integer()} ++ | {request_timeout, pos_integer() | infinity} + | {max_clients, integer()} + | {max_header_size, integer()} + | {max_content_length, integer()} +@@ -1085,9 +1087,9 @@ server. + LimitOption :: {customize, atom()} + | {disable_chunked_transfer_encoding_send, boolean()} + | {keep_alive, boolean()} +- | {keep_alive_timeout, integer()} ++ | {keep_alive_timeout, integer() | infinity} + | {max_body_size, integer()} +- | {max_body_read_timeout, pos_integer()} ++ | {request_timeout, pos_integer() | infinity} + | {max_clients, integer()} + | {max_header_size, integer()} + | {max_content_length, integer()} +@@ -1141,9 +1143,9 @@ info(Address, Port) when is_integer(Port) -> + LimitOption :: {customize, atom()} + | {disable_chunked_transfer_encoding_send, boolean()} + | {keep_alive, boolean()} +- | {keep_alive_timeout, integer()} ++ | {keep_alive_timeout, integer() | infinity} + | {max_body_size, integer()} +- | {max_body_read_timeout, pos_integer()} ++ | {request_timeout, pos_integer() | infinity} + | {max_clients, integer()} + | {max_header_size, integer()} + | {max_content_length, integer()} +@@ -1181,9 +1183,9 @@ info(Address, Port) when is_integer(Port) -> + LimitOption :: {customize, atom()} + | {disable_chunked_transfer_encoding_send, boolean()} + | {keep_alive, boolean()} +- | {keep_alive_timeout, integer()} ++ | {keep_alive_timeout, integer() | infinity} + | {max_body_size, integer()} +- | {max_body_read_timeout, pos_integer()} ++ | {request_timeout, pos_integer() | infinity} + | {max_clients, integer()} + | {max_header_size, integer()} + | {max_content_length, integer()} +@@ -1240,9 +1242,9 @@ options of the server. + LimitOption :: {customize, atom()} + | {disable_chunked_transfer_encoding_send, boolean()} + | {keep_alive, boolean()} +- | {keep_alive_timeout, integer()} ++ | {keep_alive_timeout, integer() | infinity} + | {max_body_size, integer()} +- | {max_body_read_timeout, pos_integer()} ++ | {request_timeout, pos_integer() | infinity} + | {max_clients, integer()} + | {max_header_size, integer()} + | {max_content_length, integer()} +--- a/lib/inets/src/http_server/httpd_conf.erl ++++ b/lib/inets/src/http_server/httpd_conf.erl +@@ -201,11 +201,12 @@ validate_config_params([{max_body_size, Value} | Rest]) + validate_config_params([{max_body_size, Value} | _]) -> + throw({max_body_size, Value}); + +-validate_config_params([{max_body_read_timeout, Value} | Rest]) +- when is_integer(Value) andalso (Value > 0) -> ++validate_config_params([{request_timeout, Value} | Rest]) ++ when (is_integer(Value) andalso (Value > 0)); ++ Value =:= infinity -> + validate_config_params(Rest); +-validate_config_params([{max_body_read_timeout, Value} | _]) -> +- throw({max_body_read_timeout, Value}); ++validate_config_params([{request_timeout, Value} | _]) -> ++ throw({request_timeout, Value}); + + validate_config_params([{max_content_length, Value} | Rest]) + when is_integer(Value) andalso (Value > 0) -> +@@ -280,7 +281,7 @@ validate_config_params([{max_keep_alive_request, Value} | _]) -> + throw({max_keep_alive_request, Value}); + + validate_config_params([{keep_alive_timeout, Value} | Rest]) +- when is_integer(Value) andalso (Value >= 0); ++ when (is_integer(Value) andalso (Value >= 0)); + Value =:= infinity -> + validate_config_params(Rest); + validate_config_params([{keep_alive_timeout, Value} | _]) -> +--- a/lib/inets/src/http_server/httpd_request_handler.erl ++++ b/lib/inets/src/http_server/httpd_request_handler.erl +@@ -45,9 +45,9 @@ + mfa, %% {Module, Function, Args} + max_keep_alive_request = infinity, %% integer() | infinity + response_sent = false :: boolean(), +- timeout :: pos_integer(), %% ms +- body_timeout :: pos_integer(), %% ms +- timer :: 'undefined' | {reference(), request_timeout | body_timeout}, %% Request/body timer ++ keepalive_timeout :: pos_integer(), %% ms ++ request_timeout :: pos_integer(), %% ms ++ timer :: 'undefined' | {reference(), keep_alive_timeout | request_timeout}, %% keepalive/request timer + headers = #http_request_h{}, + body, %% binary() + data, %% The total data received in bits, checked after 10s +@@ -119,7 +119,7 @@ init([Manager, ConfigDB, AcceptTimeout]) -> + Keepalive = httpd_util:lookup(ConfigDB, keep_alive, true), + %%Timeout value is in seconds we want it in milliseconds + KeepAliveTimeOut = case Keepalive of +- true -> 1000 * httpd_util:lookup(ConfigDB, keep_alive_timeout, 150); ++ true -> get_keepalive_timeout(ConfigDB); + _ -> infinity + end, + case http_transport:negotiate(SocketType, Socket, ?HANDSHAKE_TIMEOUT) of +@@ -158,7 +158,7 @@ continue_init(Manager, ConfigDB, SocketType, Socket, Peername, Sockname, + MaxURISize = max_uri_size(ConfigDB), + NrOfRequest = max_keep_alive_request(ConfigDB), + MaxContentLen = max_content_length(ConfigDB), +- MaxBodyTimeout = max_body_read_timeout(ConfigDB), ++ RequestTimeout = get_request_timeout(ConfigDB), + Customize = customize(ConfigDB), + MaxChunk = max_client_body_chunk(ConfigDB), + +@@ -185,8 +185,8 @@ continue_init(Manager, ConfigDB, SocketType, Socket, Peername, Sockname, + State = #state{mod = Mod, + manager = Manager, + status = Status, +- timeout = TimeOut, +- body_timeout = MaxBodyTimeout, ++ keepalive_timeout = TimeOut, ++ request_timeout = RequestTimeout, + max_keep_alive_request = NrOfRequest, + mfa = MFA, + chunk = chunk_start(MaxChunk)}, +@@ -243,40 +243,35 @@ handle_info({Proto, Socket, Data}, + when (((Proto =:= tcp) orelse + (Proto =:= ssl) orelse + (Proto =:= dummy)) andalso is_binary(Data)) -> +- cancel_timeout(State), ++ RequestTimeoutState = activate_request_timeout(cancel_timeout(State)), + + try Module:Function([Data | Args]) of + {ok, Result} -> +- BodyTimeoutState = activate_body_timeout(State), +- NewState = update_data_size(BodyTimeoutState, Data), ++ NewState = update_data_size(RequestTimeoutState, Data), + handle_msg(Result, NewState); + + %% error returns from httpd_request + {error, {size_error, MaxSize, ErrCode, ErrStr}, Version} -> + NewModData = ModData#mod{http_version = Version}, + httpd_response:send_status(NewModData, ErrCode, ErrStr, {max_size, MaxSize}), +- {stop, normal, State#state{response_sent = true, ++ {stop, normal, RequestTimeoutState#state{response_sent = true, + mod = NewModData}}; +- + {error, {version_error, ErrCode, ErrStr}, Version} -> + NewModData = ModData#mod{http_version = Version}, + httpd_response:send_status(NewModData, ErrCode, ErrStr), +- {stop, normal, State#state{response_sent = true, +- mod = NewModData}}; ++ {stop, normal, RequestTimeoutState#state{response_sent = true, ++ mod = NewModData}}; + {error, {bad_request, ErrCode, ErrStr}, Version} -> + NewModData = ModData#mod{http_version = Version}, + httpd_response:send_status(NewModData, ErrCode, ErrStr), +- {stop, normal, State#state{response_sent = true, +- mod = NewModData}}; +- ++ {stop, normal, RequestTimeoutState#state{response_sent = true, ++ mod = NewModData}}; + {http_chunk = Module, Function, Args} when ChunkState =/= undefined -> +- NewState = handle_chunk(Module, Function, Args, State), ++ NewState = handle_chunk(Module, Function, Args, RequestTimeoutState), + {noreply, NewState}; +- + {_M, _F, _A} = NewMFA -> + setopts(Socket, SockType, [{active, once}]), +- BodyTimeoutState = reset_body_timeout(State, Data), +- NewState = update_data_size(BodyTimeoutState, Data), ++ NewState = update_data_size(RequestTimeoutState, Data), + {noreply, NewState#state{mfa = NewMFA}} + catch throw:{error, Error} when Module =:= http_chunk -> + handle_chunk_size_error(Error, State) +@@ -735,25 +730,22 @@ handle_next_request(#state{mod = #mod{connection = true} = ModData, + handle_next_request(State, _) -> + {stop, normal, State}. + +-activate_keepalive_timeout(#state{timeout = infinity} = State) -> ++activate_keepalive_timeout(#state{keepalive_timeout = infinity} = State) -> + State; + activate_keepalive_timeout(#state{timer = {_Ref, keep_alive_timeout}} = State) -> + activate_keepalive_timeout(cancel_timeout(State)); +-activate_keepalive_timeout(#state{timeout = Time} = State) -> ++activate_keepalive_timeout(#state{timer = undefined, keepalive_timeout = Time} = State) -> + Ref = erlang:send_after(Time, self(), timeout), + State#state{timer = {Ref, keep_alive_timeout}}. + +-activate_body_timeout(#state{timer = {_Ref, body_timeout}} = State) -> +- activate_body_timeout(cancel_timeout(State)); +-activate_body_timeout(#state{timer = undefined, body_timeout = Time} = State) -> ++activate_request_timeout(#state{request_timeout = infinity} = State) -> ++ State; ++activate_request_timeout(#state{timer = {_Ref, request_timeout}} = State) -> ++ activate_request_timeout(cancel_timeout(State)); ++activate_request_timeout(#state{timer = undefined, request_timeout = Time} = State) -> + Ref = erlang:send_after(Time, self(), timeout), +- State#state{timer = {Ref, body_timeout}}; +-activate_body_timeout(State) -> +- State. +- +-reset_body_timeout(#state{timer = {_Ref, body_timeout}} = State, Data) when byte_size(Data) > 0 -> +- activate_body_timeout(cancel_timeout(State)); +-reset_body_timeout(State, _Data) -> ++ State#state{timer = {Ref, request_timeout}}; ++activate_request_timeout(State) -> + State. + + cancel_timeout(#state{timer = undefined} = State) -> +@@ -804,8 +796,17 @@ max_keep_alive_request(ConfigDB) -> + max_content_length(ConfigDB) -> + httpd_util:lookup(ConfigDB, max_content_length, ?HTTP_MAX_CONTENT_LENGTH). + +-max_body_read_timeout(ConfigDB) -> +- 1000 * httpd_util:lookup(ConfigDB, max_body_read_timeout, ?HTTP_MAX_BODY_READ_TIMEOUT). ++get_request_timeout(ConfigDB) -> ++ case httpd_util:lookup(ConfigDB, request_timeout, ?HTTP_REQUEST_READ_TIMEOUT) of ++ infinity -> infinity; ++ Val when is_integer(Val) -> 1000 * Val ++ end. ++ ++get_keepalive_timeout(ConfigDB) -> ++ case httpd_util:lookup(ConfigDB, keep_alive_timeout, 150) of ++ infinity -> infinity; ++ Val when is_integer(Val) -> 1000* Val ++ end. + + customize(ConfigDB) -> + httpd_util:lookup(ConfigDB, customize, httpd_custom). +--- a/lib/inets/test/httpd_basic_SUITE.erl ++++ b/lib/inets/test/httpd_basic_SUITE.erl +@@ -400,7 +400,7 @@ slowdose_slow_header() -> + [{doc, "Testing timeout for slow headers"}]. + slowdose_slow_header(Config) when is_list(Config) -> + HttpdConf = proplists:get_value(httpd_conf, Config), +- {ok, Pid} = inets:start(httpd, [{port, 0}, {keep_alive_timeout, 2} | HttpdConf]), ++ {ok, Pid} = inets:start(httpd, [{port, 0}, {request_timeout, 2} | HttpdConf]), + Info = httpd:info(Pid), + Port = proplists:get_value(port, Info), + {ok, Socket} = inets_test_lib:connect_bin(ip_comm, "localhost", Port, []), +@@ -423,7 +423,7 @@ slowdose_slow_body() -> + [{doc, "Testing timeout for slow body"}]. + slowdose_slow_body(Config) when is_list(Config) -> + HttpdConf = proplists:get_value(httpd_conf, Config), +- {ok, Pid} = inets:start(httpd, [{port, 0}, {max_body_read_timeout, 1} | HttpdConf]), ++ {ok, Pid} = inets:start(httpd, [{port, 0}, {request_timeout, 1} | HttpdConf]), + Info = httpd:info(Pid), + Port = proplists:get_value(port, Info), + {ok, Socket} = inets_test_lib:connect_bin(ip_comm, "localhost", Port, []), +@@ -449,10 +449,10 @@ slowdose_trickle_body() -> + [{doc, "Testing timeout for trickling body"}]. + slowdose_trickle_body(Config) when is_list(Config) -> + +- %% First we test that max_body_read_timeout does not trigger this +- slowdose_trickle_body(Config, {max_body_read_timeout, 1}, <<"501">>), ++ %% First we test that request_timeout does not trigger this ++ slowdose_trickle_body(Config, {request_timeout, 1}, <<"501">>), + +- %% Then we test that minimum_bytes_per_second does trigger it ++ % Then we test that minimum_bytes_per_second does trigger it + slowdose_trickle_body(Config, {minimum_bytes_per_second, 50}, <<"408">>). + + slowdose_trickle_body(Config, HttpdConfig, Status) -> +@@ -463,8 +463,8 @@ slowdose_trickle_body(Config, HttpdConfig, Status) -> + {ok, Socket} = inets_test_lib:connect_bin(ip_comm, "localhost", Port, []), + ok = inets_test_lib:send(ip_comm, Socket, "POST /dummy.html HTTP/1.1\r\n" ++ + "Host: localhost\r\n" ++ +- "Content-Length: 10\r\n\r\n"), +- [timer:sleep(500), inets_test_lib:send(ip_comm, Socket, integer_to_list(N)) || N <- lists:seq(1, 10)], ++ "Content-Length: 11\r\n\r\n"), ++ [inets_test_lib:send(ip_comm, Socket, integer_to_list(N)) || N <- lists:seq(1, 10), is_atom(timer:sleep(500))], + receive + {tcp, Socket, Data} -> + case binary:match(Data, Status) of +@@ -478,7 +478,9 @@ slowdose_trickle_body(Config, HttpdConfig, Status) -> + ct:fail({"Unexpected data: ", Else1}) + after 3000 -> + ct:fail("Server did not close connection after slow header") +- end. ++ end, ++ inets_test_lib:close(ip_comm, Socket), ++ ok = inets:stop(httpd, Pid). + + %%------------------------------------------------------------------------- + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-5.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-5.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-5.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-5.patch 2026-10-19 10:48:03.000000000 +0000 @@ -0,0 +1,68 @@ +From: Konrad Pietrzak +Date: Mon, 24 Aug 2026 11:11:56 +0200 +Subject: Fix dialyzer errors in httpd_request_handler + +Origin: upstream, https://github.com/erlang/otp/commit/10185f3ff9be205cdaa3f6331256f9b8bb91bbc2 +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/inets/src/http_server/httpd_request_handler.erl | 19 ++++++++----------- + 1 file changed, 8 insertions(+), 11 deletions(-) + +--- a/lib/inets/src/http_server/httpd_request_handler.erl ++++ b/lib/inets/src/http_server/httpd_request_handler.erl +@@ -45,9 +45,9 @@ + mfa, %% {Module, Function, Args} + max_keep_alive_request = infinity, %% integer() | infinity + response_sent = false :: boolean(), +- keepalive_timeout :: pos_integer(), %% ms +- request_timeout :: pos_integer(), %% ms +- timer :: 'undefined' | {reference(), keep_alive_timeout | request_timeout}, %% keepalive/request timer ++ keepalive_timeout :: pos_integer() | infinity, %% ms ++ request_timeout :: pos_integer() | infinity, %% ms ++ timer = undefined :: {reference(), keep_alive_timeout | request_timeout} | undefined, %% keepalive/request timer + headers = #http_request_h{}, + body, %% binary() + data, %% The total data received in bits, checked after 10s +@@ -243,7 +243,8 @@ handle_info({Proto, Socket, Data}, + when (((Proto =:= tcp) orelse + (Proto =:= ssl) orelse + (Proto =:= dummy)) andalso is_binary(Data)) -> +- RequestTimeoutState = activate_request_timeout(cancel_timeout(State)), ++ ++ RequestTimeoutState = activate_request_timeout(State), + + try Module:Function([Data | Args]) of + {ok, Result} -> +@@ -732,21 +733,17 @@ handle_next_request(State, _) -> + + activate_keepalive_timeout(#state{keepalive_timeout = infinity} = State) -> + State; +-activate_keepalive_timeout(#state{timer = {_Ref, keep_alive_timeout}} = State) -> +- activate_keepalive_timeout(cancel_timeout(State)); + activate_keepalive_timeout(#state{timer = undefined, keepalive_timeout = Time} = State) -> + Ref = erlang:send_after(Time, self(), timeout), + State#state{timer = {Ref, keep_alive_timeout}}. + + activate_request_timeout(#state{request_timeout = infinity} = State) -> + State; +-activate_request_timeout(#state{timer = {_Ref, request_timeout}} = State) -> ++activate_request_timeout(#state{timer = {_Ref, _}} = State) -> + activate_request_timeout(cancel_timeout(State)); + activate_request_timeout(#state{timer = undefined, request_timeout = Time} = State) -> + Ref = erlang:send_after(Time, self(), timeout), +- State#state{timer = {Ref, request_timeout}}; +-activate_request_timeout(State) -> +- State. ++ State#state{timer = {Ref, request_timeout}}. + + cancel_timeout(#state{timer = undefined} = State) -> + State; +@@ -805,7 +802,7 @@ get_request_timeout(ConfigDB) -> + get_keepalive_timeout(ConfigDB) -> + case httpd_util:lookup(ConfigDB, keep_alive_timeout, 150) of + infinity -> infinity; +- Val when is_integer(Val) -> 1000* Val ++ Val when is_integer(Val) -> 1000 * Val + end. + + customize(ConfigDB) -> diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-6.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-6.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-6.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-71380-6.patch 2026-09-30 10:13:04.000000000 +0000 @@ -0,0 +1,42 @@ +From: Konrad Pietrzak +Date: Mon, 31 Aug 2026 19:04:50 +0200 +Subject: Gate keep_alive_timeout in httpd_bench_SUITE + +Origin: upstream, https://github.com/erlang/otp/commit/3bdf3b1ff73efe4e39c6d924b20140eb49bdb08e +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/inets/test/httpd_bench_SUITE.erl | 23 ++++++++++++-------- + 1 file changed, 14 insertions(+), 9 deletions(-) + +--- a/lib/inets/test/httpd_bench_SUITE.erl ++++ b/lib/inets/test/httpd_bench_SUITE.erl +@@ -477,15 +477,20 @@ start_inets(Protocol, ConfHttpd, Config) -> + Node = ?config(server_node, Config), + Host = ?config(server_host, Config), + HTTPVersion = ?config(http_version, Config), +- Conf = [httpd, [{port,0}, +- {http_version, HTTPVersion}, +- {ipfamily, inet}, +- {server_name, "inets_test"}, +- {server_root, PrivDir}, +- {document_root, DataDir}, +- {keep_alive, ?config(keep_alive, Config)}, +- {keep_alive_timeout, 360} +- | ConfHttpd]], ++ ++ Conf0 = [{port,0}, ++ {http_version, HTTPVersion}, ++ {ipfamily, inet}, ++ {server_name, "inets_test"}, ++ {server_root, PrivDir}, ++ {document_root, DataDir}, ++ {keep_alive, ?config(keep_alive, Config)} ++ | ConfHttpd], ++ ++ Conf = case ?config(keep_alive, Config) of ++ true -> [httpd, [{keep_alive_timeout, 360} | Conf0]]; ++ false -> [httpd, Conf0] ++ end, + {ok, Pid} = rpc:call(Node, inets, start, Conf), + Port = proplists:get_value(port, rpc:call(Node, httpd, info, [Pid])), + F = fun(File) -> diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73270-1.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73270-1.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73270-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73270-1.patch 2026-10-19 10:48:41.000000000 +0000 @@ -0,0 +1,222 @@ +From: Konrad Pietrzak +Date: Fri, 7 Aug 2026 13:12:11 +0200 +Subject: inets: Canonicalize request path before mod_auth directory check + + The mod_auth directory-matching logic uses re:run/3 to check whether + a resolved filesystem path falls inside a protected directory block. + This comparison is sensitive to path variants that the operating system + treats as equivalent: repeated slashes (//) and, on case-insensitive + filesystems (macOS, Windows), case differences. + + Collapse consecutive slashes and, when the underlying filesystem is + case-insensitive, normalize case before the authorization decision so + that mod_auth, mod_security, and mod_get all operate on the same + canonical path. + +Origin: upstream, https://github.com/erlang/otp/commit/bac19eb3dbd96cc49b6d8cabc1c04248bf8c79f6 +Bug-Debian: https://bugs.debian.org/1146603 +Comment: Also fixes CVE-2026-66835 (same upstream commits, OTP-20279). +--- + lib/inets/src/http_server/httpd_request.erl | 17 +++- + lib/inets/src/http_server/httpd_util.erl | 23 ++++- + lib/inets/src/http_server/mod_alias.erl | 2 +- + lib/inets/src/http_server/mod_auth.erl | 2 +- + lib/inets/test/httpd_SUITE.erl | 95 +++++++++++++++++++- + 5 files changed, 134 insertions(+), 5 deletions(-) + +--- a/lib/inets/src/http_server/httpd_request.erl ++++ b/lib/inets/src/http_server/httpd_request.erl +@@ -341,7 +341,22 @@ validate_uri(RequestURI) -> + {error, _, _} -> + {error, {bad_request, {malformed_syntax, RequestURI}}}; + URI -> +- {ok, URI} ++ {ok, collapse_uri_path_slashes(URI)} ++ end. ++ ++%% Collapse consecutive slashes in the path component only. ++%% Uses uri_string:parse/1 to avoid mangling "://" in absolute URIs. ++collapse_uri_path_slashes([$/ | _] = Path) -> ++ %% Path-only form (the common case for httpd requests). ++ httpd_util:collapse_slashes(Path); ++collapse_uri_path_slashes(URI) -> ++ case uri_string:parse(URI) of ++ #{path := Path} = Parsed when map_size(Parsed) > 1 -> ++ %% Absolute URI — collapse only the path, recompose. ++ uri_string:recompose(Parsed#{path => httpd_util:collapse_slashes(Path)}); ++ _ -> ++ %% Unparseable or path-only without leading slash. ++ httpd_util:collapse_slashes(URI) + end. + + validate_version("HTTP/1.1") -> +--- a/lib/inets/src/http_server/httpd_util.erl ++++ b/lib/inets/src/http_server/httpd_util.erl +@@ -72,7 +72,8 @@ miscellaneous utility functions. + mime_type_validate/1, + mime_types_validate/1, + custom_date/0, +- error_log/2]). ++ error_log/2, ++ collapse_slashes/1]). + + -removed({flatlength, 1, "use erlang:iolist_size/1 instead"}). + -removed({hexlist_to_integer, 1, "use erlang:list_to_integer/2 with base 16 instead"}). +@@ -926,3 +927,23 @@ mod_error_logging(Mod, ConfigDB, Report) -> + _ -> + ok + end. ++ ++%%---------------------------------------------------------------------- ++%% collapse_slashes(Path) -> Path ++%% ++%% Collapse consecutive slash characters into a single slash. ++%% This ensures the authorization decision (mod_auth, mod_security) ++%% and the file-serving layer (mod_get) see the same canonical path. ++%% Without this, "//secret/file" bypasses mod_auth's re:run directory ++%% check while the OS still serves the file (treating // as /). ++%%---------------------------------------------------------------------- ++-doc false. ++collapse_slashes(Path) -> ++ collapse_slashes(Path, []). ++ ++collapse_slashes([], Acc) -> ++ lists:reverse(Acc); ++collapse_slashes([$/,$/ | Rest], Acc) -> ++ collapse_slashes([$/ | Rest], Acc); ++collapse_slashes([C | Rest], Acc) -> ++ collapse_slashes(Rest, [C | Acc]). +\ No newline at end of file +--- a/lib/inets/src/http_server/mod_alias.erl ++++ b/lib/inets/src/http_server/mod_alias.erl +@@ -275,7 +275,7 @@ percent_decode_path(InitPath) -> + {error, _, _} -> + InitPath; + Path -> +- Path ++ httpd_util:collapse_slashes(Path) + end + end. + %% +--- a/lib/inets/src/http_server/mod_auth.erl ++++ b/lib/inets/src/http_server/mod_auth.erl +@@ -777,7 +777,7 @@ secret_path(_Path, [], to_be_found) -> + secret_path(_Path, [], Directory) -> + {yes, Directory}; + secret_path(Path, [[NewDirectory] | Rest], Directory) -> +- case re:run(Path, NewDirectory, [{capture, first}]) of ++ case re:run(Path, NewDirectory, [{capture, first}, caseless]) of + {match, _} when Directory =:= to_be_found -> + secret_path(Path, Rest, NewDirectory); + {match, [{_, Length}]} when Length > length(Directory)-> +--- a/lib/inets/test/httpd_SUITE.erl ++++ b/lib/inets/test/httpd_SUITE.erl +@@ -132,7 +132,9 @@ groups() -> + post_204, + chunked_invalid_chunk_size, + multiple_content_length_header]}, +- {basic_auth, [], [basic_auth_1_1, basic_auth_1_0, verify_href_1_1]}, ++ {basic_auth, [], [basic_auth_1_1, basic_auth_1_0, verify_href_1_1, ++ double_slash_auth_bypass, case_insensitive_auth_bypass, ++ auth_path_canonicalization]}, + {auth_api, [], [auth_api_1_1, auth_api_1_0]}, + {auth_api_dets, [], [auth_api_1_1, auth_api_1_0]}, + {auth_api_mnesia, [], [auth_api_1_1, auth_api_1_0]}, +@@ -612,6 +614,97 @@ verify_href(Config) when is_list(Config) -> + [ok = Go(H, "one", "onePassword", [{statuscode, 200}]) || H <- Hrefs], + ok. + ++%%-------------------------------------------------------------------- ++%% Test that double slashes in the request path do not bypass ++%% mod_auth directory protection (CVE-2026-28808 related). ++%%-------------------------------------------------------------------- ++double_slash_auth_bypass() -> ++ [{doc, "Verify that // in request path does not bypass mod_auth"}]. ++ ++double_slash_auth_bypass(Config) when is_list(Config) -> ++ Version = proplists:get_value(http_version, Config), ++ Host = proplists:get_value(host, Config), ++ %% Without credentials, all these variants must return 401 ++ %% (protected by mod_auth directory config for /open/). ++ Paths = [ ++ "/open/dummy.html", %% normal path (baseline) ++ "//open/dummy.html", %% leading double slash ++ "/open//dummy.html", %% double slash inside path ++ "///open///dummy.html", %% triple slashes ++ "/open/./dummy.html" %% dot segment (should still match) ++ ], ++ [ok = http_status("GET " ++ Path ++ " ", Config, ++ [{statuscode, 401}, ++ {header, "WWW-Authenticate"}]) ++ || Path <- Paths], ++ %% With valid credentials, the same paths must return 200. ++ [ok = auth_status(auth_request(Path, "one", "onePassword", Version, Host), ++ Config, [{statuscode, 200}]) ++ || Path <- Paths], ++ %% Also verify /secret/ with double-slash variants. ++ SecretPaths = [ ++ "//secret/dummy.html", ++ "/secret//dummy.html", ++ "///secret/dummy.html" ++ ], ++ [ok = http_status("GET " ++ Path ++ " ", Config, ++ [{statuscode, 401}, ++ {header, "WWW-Authenticate"}]) ++ || Path <- SecretPaths], ++ ok. ++ ++%%-------------------------------------------------------------------- ++%% Test that case variations in the request path do not bypass ++%% mod_auth directory protection on case-insensitive filesystems. ++%% On Linux (case-sensitive FS) the file won't be found, but the ++%% auth check must still trigger (401) rather than being skipped. ++%%-------------------------------------------------------------------- ++case_insensitive_auth_bypass() -> ++ [{doc, "Verify that case variations in path do not bypass mod_auth"}]. ++ ++case_insensitive_auth_bypass(Config) when is_list(Config) -> ++ %% Without credentials, case-varied paths to protected dirs must ++ %% return 401 (auth required), NOT 404 or 200. ++ %% The caseless flag in mod_auth ensures the directory regex matches ++ %% regardless of case. ++ Paths = [ ++ "/Open/dummy.html", %% capital O ++ "/OPEN/dummy.html", %% all caps ++ "/Secret/dummy.html", %% capital S ++ "/SECRET/dummy.html", %% all caps ++ "/sEcReT/dummy.html" %% mixed case ++ ], ++ [ok = http_status("GET " ++ Path ++ " ", Config, ++ [{statuscode, 401}, ++ {header, "WWW-Authenticate"}]) ++ || Path <- Paths], ++ ok. ++ ++%%-------------------------------------------------------------------- ++%% Unit test for httpd_util:collapse_slashes/1 ++%%-------------------------------------------------------------------- ++auth_path_canonicalization() -> ++ [{doc, "Unit test for httpd_util:collapse_slashes/1"}]. ++ ++auth_path_canonicalization(Config) when is_list(Config) -> ++ %% Basic collapse cases ++ "/" = httpd_util:collapse_slashes("/"), ++ "/a/b/c" = httpd_util:collapse_slashes("/a/b/c"), ++ "/a/b/c" = httpd_util:collapse_slashes("//a//b//c"), ++ "/a/b/c/" = httpd_util:collapse_slashes("///a///b///c/"), ++ "/open/dummy.html" = httpd_util:collapse_slashes("//open//dummy.html"), ++ "/secret/top_secret/" = httpd_util:collapse_slashes("/secret//top_secret//"), ++ %% Empty string ++ "" = httpd_util:collapse_slashes(""), ++ %% No slashes at all ++ "abc" = httpd_util:collapse_slashes("abc"), ++ %% Single slash preserved ++ "/" = httpd_util:collapse_slashes("//"), ++ "/" = httpd_util:collapse_slashes("///"), ++ %% Mixed content ++ "/foo/bar/baz" = httpd_util:collapse_slashes("/foo///bar//baz"), ++ ok. ++ + auth_api_1_1(Config) when is_list(Config) -> + auth_api([{http_version, "HTTP/1.1"} | Config]). + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73270-2.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73270-2.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73270-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73270-2.patch 2026-10-19 10:48:52.000000000 +0000 @@ -0,0 +1,36 @@ +From: Konrad Pietrzak +Date: Thu, 20 Aug 2026 16:31:58 +0200 +Subject: inets: Add caseless matching to mod_security directory lookup + + Add the caseless option to the re:run/3 call in secret_path/3, + mirroring the mod_auth fix, so mod_security attributes requests + to the correct directory regardless of path case. + +Origin: upstream, https://github.com/erlang/otp/commit/15ca963c351fd314bde5faa2eaeccb25ecf4754e +Bug-Debian: https://bugs.debian.org/1146603 +Comment: Also fixes CVE-2026-66835 (same upstream commits, OTP-20279). +--- + lib/inets/src/http_server/httpd_util.erl | 2 +- + lib/inets/src/http_server/mod_security.erl | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +--- a/lib/inets/src/http_server/httpd_util.erl ++++ b/lib/inets/src/http_server/httpd_util.erl +@@ -946,4 +946,4 @@ collapse_slashes([], Acc) -> + collapse_slashes([$/,$/ | Rest], Acc) -> + collapse_slashes([$/ | Rest], Acc); + collapse_slashes([C | Rest], Acc) -> +- collapse_slashes(Rest, [C | Acc]). +\ No newline at end of file ++ collapse_slashes(Rest, [C | Acc]). +--- a/lib/inets/src/http_server/mod_security.erl ++++ b/lib/inets/src/http_server/mod_security.erl +@@ -374,7 +374,7 @@ secret_path(_Path, [], to_be_found) -> + secret_path(_Path, [], Dir) -> + {yes, Dir}; + secret_path(Path, [[NewDir]|Rest], Dir) -> +- case re:run(Path, NewDir, [{capture, first}]) of ++ case re:run(Path, NewDir, [{capture, first}, caseless]) of + {match, _} when Dir =:= to_be_found -> + secret_path(Path, Rest, NewDir); + {match, [{_, Length}]} when Length > length(Dir) -> diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73276-1.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73276-1.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73276-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73276-1.patch 2026-10-19 10:49:19.000000000 +0000 @@ -0,0 +1,100 @@ +From: Konrad Pietrzak +Date: Mon, 27 Jul 2026 15:21:56 +0200 +Subject: inets: Reject headers with whitespace before colon in httpd + + "Content-Length : 6" (space before colon) was silently dropped by + key_value/1, causing content-length to default to "0". The orphaned + body bytes were then reinterpreted as the next pipelined request via + body_data/2, enabling CL.0 request smuggling/desync. + + RFC 7230 Section 3.2.4 states that no whitespace is allowed between + the header field-name and colon, and such requests must be rejected. + + Return {error, whitespace_before_colon} from key_value/1 instead of + undefined for this case. Handle it in parse_headers/7 by returning + {error, {bad_request, 400, _}, Version}. + +Origin: upstream, https://github.com/erlang/otp/commit/60add5a19e2154560fdff3fe92dba14a68bbd683 +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/inets/src/http_lib/http_request.erl | 11 +++++------ + lib/inets/src/http_server/httpd_request.erl | 12 +++++++++--- + lib/inets/test/httpd_SUITE.erl | 4 +++- + 3 files changed, 17 insertions(+), 10 deletions(-) + +--- a/lib/inets/src/http_lib/http_request.erl ++++ b/lib/inets/src/http_lib/http_request.erl +@@ -29,18 +29,17 @@ + key_value(KeyValueStr) -> + case lists:splitwith(fun($:) -> false; (_) -> true end, KeyValueStr) of + {Key, [$: | Value]} when Key =/= [] -> +- %% RFC 7230 - 3.2.4 ... No whitespace is allowed between the header field-name and colon. ++ %% RFC 7230 - 3.2.4 ... No whitespace is allowed between the header field-name and colon. + case string:strip(Key, right) of + Key -> + {http_util:to_lower(string:strip(Key, left)), string:strip(Value)}; +- _ -> +- %% Ignore invalid header +- undefined ++ _ -> ++ {error, whitespace_before_colon} + end; +- {_, []} -> ++ {_, []} -> + undefined; + _ -> +- undefined ++ undefined + end. + %%------------------------------------------------------------------------- + %% headers(HeaderList, #http_request_h{}) -> #http_request_h{} +--- a/lib/inets/src/http_server/httpd_request.erl ++++ b/lib/inets/src/http_server/httpd_request.erl +@@ -204,12 +204,15 @@ parse_headers(<>, Header, Headers, _, _, + Options, Result) -> + Customize = proplists:get_value(customize, Options), + case http_request:key_value(lists:reverse(Header)) of +- undefined -> %% Skip invalid headers ++ undefined -> %% Skip headers with missing : + FinalHeaders = lists:filtermap(fun(H) -> + httpd_custom:customize_headers(Customize, request_header, H) + end, + Headers), + {ok, list_to_tuple(lists:reverse([Body, {http_request:headers(FinalHeaders, #http_request_h{}), FinalHeaders} | Result]))}; ++ {error, whitespace_before_colon} -> ++ HttpVersion = lists:nth(3, lists:reverse(Result)), ++ {error, {bad_request, 400, "whitespace before colon in header"}, HttpVersion}; + NewHeader -> + case check_header(NewHeader, Headers, Options) of + ok -> +@@ -263,12 +266,15 @@ parse_headers(<>, Header, Headers, Current, Max, + Options, Result) -> + case http_request:key_value(lists:reverse(Header)) of + undefined -> %% Skip headers with missing : +- parse_headers(Rest, [Octet], Headers, ++ parse_headers(Rest, [Octet], Headers, + Current, Max, Options, Result); ++ {error, whitespace_before_colon} -> ++ HttpVersion = lists:nth(3, lists:reverse(Result)), ++ {error, {bad_request, 400, "whitespace before colon in header"}, HttpVersion}; + NewHeader -> + case check_header(NewHeader, Headers, Options) of + ok -> +- parse_headers(Rest, [Octet], [NewHeader | Headers], ++ parse_headers(Rest, [Octet], [NewHeader | Headers], + Current, Max, Options, Result); + {error, Reason} -> + HttpVersion = lists:nth(3, lists:reverse(Result)), +--- a/lib/inets/test/httpd_SUITE.erl ++++ b/lib/inets/test/httpd_SUITE.erl +@@ -1805,7 +1805,9 @@ ignore_invalid_header(Config) when is_list(Config) -> + {"https://" ++ Host ++ ":" ++ integer_to_list(Port) ++ "/cgi-bin/erl/httpd_example:ignore_invalid_header", + [{"Host", "localhost"},{"Te", ""}, {"Content-Length ", "0"}], [{ssl, [{verify, verify_none} | Conf]}]} + end, +- {ok,{{_,204,_}, _, _}} ++ %% RFC 7230 Section 3.2.4: No whitespace allowed between header ++ %% field-name and colon. Server MUST reject with 400. ++ {ok,{{_,400,_}, _, _}} + = httpc:request(get, {Url, Header}, [{timeout, 45000} | Opts], [{headers_as_is, true}]). + + %%------------------------------------------------------------------------- diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73276-2.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73276-2.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73276-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73276-2.patch 2026-10-19 10:49:29.000000000 +0000 @@ -0,0 +1,96 @@ +From: Konrad Pietrzak +Date: Thu, 20 Aug 2026 17:52:52 +0200 +Subject: httpd: add error handling for headers with whitespace before colon + +Origin: upstream, https://github.com/erlang/otp/commit/226f936eb005d6a8f0130a2110453763a8b82f2d +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/inets/src/http_lib/http_request.erl | 8 +++++--- + lib/inets/src/http_server/httpd_internal.hrl | 1 + + lib/inets/src/http_server/httpd_request.erl | 20 +++++++++++++++----- + 3 files changed, 21 insertions(+), 8 deletions(-) + +--- a/lib/inets/src/http_lib/http_request.erl ++++ b/lib/inets/src/http_lib/http_request.erl +@@ -30,9 +30,9 @@ key_value(KeyValueStr) -> + case lists:splitwith(fun($:) -> false; (_) -> true end, KeyValueStr) of + {Key, [$: | Value]} when Key =/= [] -> + %% RFC 7230 - 3.2.4 ... No whitespace is allowed between the header field-name and colon. +- case string:strip(Key, right) of ++ case string:trim(Key, trailing) of + Key -> +- {http_util:to_lower(string:strip(Key, left)), string:strip(Value)}; ++ {http_util:to_lower(string:trim(Key, leading)), string:trim(Value)}; + _ -> + {error, whitespace_before_colon} + end; +@@ -42,7 +42,7 @@ key_value(KeyValueStr) -> + undefined + end. + %%------------------------------------------------------------------------- +-%% headers(HeaderList, #http_request_h{}) -> #http_request_h{} ++%% headers(HeaderList, #http_request_h{}) -> #http_request_h{} | {error, term()} + %% HeaderList - ["HeaderField:Value"] + %% HeaderField - string() + %% Value - string() +@@ -52,6 +52,8 @@ key_value(KeyValueStr) -> + %%------------------------------------------------------------------------- + headers([], Headers) -> + Headers; ++headers([{error, _} = Error | _], _Headers) -> ++ Error; + headers([{Key, Value} | Tail], Headers) -> + headers(Tail, headers(Key, Value, Headers)); + headers([undefined], Headers) -> +--- a/lib/inets/src/http_server/httpd_internal.hrl ++++ b/lib/inets/src/http_server/httpd_internal.hrl +@@ -41,5 +41,6 @@ + {sizefmt,"abbrev"}]). + + -define(CL_TE_ERROR, "Transfer-Encoding and Content-Length headers in the same request are forbidden"). ++-define(ERROR_WHITESPACE_BEFORE_COLON, "Whitespace before colon in header"). + + -endif. % -ifdef(httpd_internal_hrl). +--- a/lib/inets/src/http_server/httpd_request.erl ++++ b/lib/inets/src/http_server/httpd_request.erl +@@ -203,25 +203,35 @@ parse_headers(<>, [], [], _, _, _, Result) -> + parse_headers(<>, Header, Headers, _, _, + Options, Result) -> + Customize = proplists:get_value(customize, Options), ++ HttpVersion = lists:nth(3, lists:reverse(Result)), + case http_request:key_value(lists:reverse(Header)) of + undefined -> %% Skip headers with missing : + FinalHeaders = lists:filtermap(fun(H) -> + httpd_custom:customize_headers(Customize, request_header, H) + end, + Headers), +- {ok, list_to_tuple(lists:reverse([Body, {http_request:headers(FinalHeaders, #http_request_h{}), FinalHeaders} | Result]))}; ++ case http_request:headers(FinalHeaders, #http_request_h{}) of ++ {error, withespace_before_colon} -> ++ {error, {bad_request, 400, ?ERROR_WHITESPACE_BEFORE_COLON}, HttpVersion}; ++ ParsedHeaders -> ++ {ok, list_to_tuple(lists:reverse([Body, {ParsedHeaders, FinalHeaders} | Result]))} ++ end; + {error, whitespace_before_colon} -> + HttpVersion = lists:nth(3, lists:reverse(Result)), +- {error, {bad_request, 400, "whitespace before colon in header"}, HttpVersion}; ++ {error, {bad_request, 400, ?ERROR_WHITESPACE_BEFORE_COLON}, HttpVersion}; + NewHeader -> + case check_header(NewHeader, Headers, Options) of + ok -> + FinalHeaders = lists:filtermap(fun(H) -> + httpd_custom:customize_headers(Customize, request_header, H) + end, [NewHeader | Headers]), +- {ok, list_to_tuple(lists:reverse([Body, {http_request:headers(FinalHeaders, +- #http_request_h{}), +- FinalHeaders} | Result]))}; ++ ++ case http_request:headers(FinalHeaders, #http_request_h{}) of ++ {error, whitespace_before_colon} -> ++ {error, {bad_request, 400, ?ERROR_WHITESPACE_BEFORE_COLON}, HttpVersion}; ++ ParsedHeaders -> ++ {ok, list_to_tuple(lists:reverse([Body, {ParsedHeaders, FinalHeaders} | Result]))} ++ end; + + {error, Reason} -> + HttpVersion = lists:nth(3, lists:reverse(Result)), diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73812.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73812.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73812.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-73812.patch 2026-10-19 10:49:44.000000000 +0000 @@ -0,0 +1,210 @@ +From: Konrad Pietrzak +Date: Mon, 27 Jul 2026 12:37:56 +0200 +Subject: inets: Reject requests with both Transfer-Encoding and Content-Length + + A request containing both Transfer-Encoding and Content-Length + headers is now rejected with 400 Bad Request, as recommended by + RFC 9112 Section 6.3. Such a message indicates a potential request + smuggling attempt (CL.TE desync) and ought to be handled as an + error. + + Add a bad_request error handler in httpd_request_handler to ensure + a proper 400 response is sent (instead of dropping the connection) + when the parser rejects a request. + +Origin: upstream, https://github.com/erlang/otp/commit/591dc00dc99dc2a426167a3b5257c0c94bd45e91 +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/inets/src/http_server/httpd_internal.hrl | 1 + + lib/inets/src/http_server/httpd_request.erl | 20 +++++++-- + lib/inets/test/http_test_lib.erl | 43 +++++++++++++++++-- + lib/inets/test/httpc_SUITE.erl | 2 - + lib/inets/test/httpd_SUITE.erl | 44 +++++++++++++++++++- + 5 files changed, 101 insertions(+), 9 deletions(-) + +--- a/lib/inets/src/http_server/httpd_internal.hrl ++++ b/lib/inets/src/http_server/httpd_internal.hrl +@@ -40,5 +40,6 @@ + {timefmt,"%A, %d-%b-%y %T %Z"}, + {sizefmt,"abbrev"}]). + ++-define(CL_TE_ERROR, "Transfer-Encoding and Content-Length headers in the same request are forbidden"). + + -endif. % -ifdef(httpd_internal_hrl). +--- a/lib/inets/src/http_server/httpd_request.erl ++++ b/lib/inets/src/http_server/httpd_request.erl +@@ -451,11 +451,25 @@ default_version()-> + "HTTP/1.1". + + check_header({"content-length", Value}, Headers, MaxSizes) -> +- case check_parsed_content_length_values(Value, Headers) of ++ case lists:keymember("transfer-encoding", 1, Headers) of + true -> +- check_content_length_value(Value, MaxSizes); ++ {error, {bad_request, 400, ?CL_TE_ERROR}}; + false -> +- {error, {bad_request, 400, "Multiple Content-Length headers with different values"}} ++ case check_parsed_content_length_values(Value, Headers) of ++ true -> ++ check_content_length_value(Value, MaxSizes); ++ false -> ++ {error, {bad_request, 400, ++ "Multiple Content-Length headers with different values"}} ++ end ++ end; ++ ++check_header({"transfer-encoding", _Value}, Headers, _MaxSizes) -> ++ case lists:keymember("content-length", 1, Headers) of ++ true -> ++ {error, {bad_request, 400, ?CL_TE_ERROR}}; ++ false -> ++ ok + end; + + check_header(_, _, _) -> +--- a/lib/inets/test/http_test_lib.erl ++++ b/lib/inets/test/http_test_lib.erl +@@ -151,7 +151,8 @@ dummy_request_handler_init(MFA, Socket0, ContentCb, Conf) -> + dummy_request_handler_loop({Module, Function, Args}, SockType, Socket, ContentCb, Conf) -> + receive + {Proto, _, Data} when (Proto =:= tcp) orelse (Proto =:= ssl) -> +- case handle_request(Module, Function, [Data | Args], Socket, ContentCb, Conf) of ++ case handle_request(Module, Function, [Data | Args], SockType, ++ Socket, ContentCb, Conf) of + stop when Proto =:= tcp -> + gen_tcp:close(Socket); + stop when Proto =:= ssl -> +@@ -165,7 +166,7 @@ dummy_request_handler_loop({Module, Function, Args}, SockType, Socket, ContentCb + ssl:close(Socket) + end. + +-handle_request(Module, Function, Args, Socket, ContentCb, Conf) -> ++handle_request(Module, Function, Args, SockType, Socket, ContentCb, Conf) -> + case Module:Function(Args) of + {ok, Result} -> + case ContentCb:handle_http_msg(Result, Socket, Conf) of +@@ -187,12 +188,48 @@ handle_request(Module, Function, Args, Socket, ContentCb, Conf) -> + {max_method, ?HTTP_MAX_METHOD_STRING}, + {max_content_length, ?HTTP_MAX_CONTENT_LENGTH}, + {customize, httpd_custom} +- ]], Socket, ContentCb, Conf) ++ ]], SockType, Socket, ContentCb, Conf) + end; ++ ++ %% The request was rejected by the parser. Mirror what ++ %% httpd_request_handler does and send a status response, then ++ %% close. Without this the error tuple is indistinguishable from ++ %% an {M, F, A} continuation, so the handler would silently wait ++ %% for more data and the client would hang until its timetrap ++ %% expires instead of failing on the unexpected response. ++ {error, {size_error, _MaxSize, StatusCode, Description}, Version} -> ++ send_status(SockType, Socket, Version, StatusCode, Description), ++ stop; ++ {error, {version_error, StatusCode, Description}, Version} -> ++ send_status(SockType, Socket, Version, StatusCode, Description), ++ stop; ++ {error, {bad_request, StatusCode, Description}, Version} ++ when is_integer(StatusCode) -> ++ send_status(SockType, Socket, Version, StatusCode, Description), ++ stop; ++ {error, Reason, Version} -> ++ ct:log("Dummy server rejected request: ~p", [Reason]), ++ send_status(SockType, Socket, Version, 400, "Bad Request"), ++ stop; ++ {error, Reason} -> ++ ct:log("Dummy server rejected request: ~p", [Reason]), ++ send_status(SockType, Socket, "HTTP/1.1", 400, "Bad Request"), ++ stop; ++ + NewMFA -> + NewMFA + end. + ++send_status(SockType, Socket, Version, StatusCode, Description) -> ++ Phrase = httpd_util:reason_phrase(StatusCode), ++ Body = "

" ++ integer_to_list(StatusCode) ++ " " ++ Phrase ++ ++ "

" ++ Description ++ "", ++ Response = Version ++ " " ++ integer_to_list(StatusCode) ++ " " ++ Phrase ++ ++ "\r\nContent-Type: text/html" ++ ++ "\r\nContent-Length: " ++ integer_to_list(length(Body)) ++ ++ "\r\nConnection: close\r\n\r\n" ++ Body, ++ inets_test_lib:send(SockType, Socket, Response). ++ + %% Perform a synchronous stop + dummy_server_stop(Pid) -> + Pid ! {stop, self()}, +--- a/lib/inets/test/httpc_SUITE.erl ++++ b/lib/inets/test/httpc_SUITE.erl +@@ -1353,12 +1353,10 @@ headers_dummy(Config) when is_list(Config) -> + {"Authorization", Auth}, + {"Expect", "1#100-continue"}, + {"User-Agent","inets"}, +- {"Transfer-Encoding","chunked"}, + {"Range", " bytes=0-499"}, + {"If-Range", "Sat, 29 Oct 1994 19:43:31 GMT"}, + {"If-Match", "*"}, + {"Content-Type", "text/plain"}, +- {"Content-Length", "6"}, + {"Content-Language", "en"}, + {"Content-Location", "http://www.foobar.se"}, + {"Content-MD5", +--- a/lib/inets/test/httpd_SUITE.erl ++++ b/lib/inets/test/httpd_SUITE.erl +@@ -141,7 +141,7 @@ groups() -> + {security, [], [security_1_1, security_1_0]}, + {logging, [], [disk_log_internal, disk_log_exists, + disk_log_bad_size, disk_log_bad_file]}, +- {http_1_1, [], [esi_propagate, esi_atom_leak, {group, http_1_1_parallel}, ++ {http_1_1, [], [esi_propagate, esi_atom_leak, te_cl_smuggling_reject, {group, http_1_1_parallel}, + cgi_bin_env] ++ load()}, + {http_1_1_parallel, [parallel], + [host, chunked, expect, cgi, cgi_chunked_encoding_test, +@@ -1135,6 +1135,48 @@ expect(Config) when is_list(Config) -> + httpd_1_1:expect(proplists:get_value(type, Config), proplists:get_value(port, Config), + proplists:get_value(host, Config), proplists:get_value(node, Config)). + %%------------------------------------------------------------------------- ++te_cl_smuggling_reject() -> ++ [{doc, "Verify that requests with both Transfer-Encoding and " ++ "Content-Length are rejected with 400 (RFC 9112 Section 6.3). " ++ "Prevents CL.TE request smuggling."}]. ++ ++te_cl_smuggling_reject(Config) when is_list(Config) -> ++ %% Target a resource that accepts POST. A static file answers 501 ++ %% regardless of framing, which would hide the difference between an ++ %% accepted and a rejected request. ++ Request = "POST /cgi-bin/erl/httpd_example/post ", ++ ++ %% Test 1: Transfer-Encoding before Content-Length ++ ok = http_status(Request, ++ {"Transfer-Encoding:chunked\r\n" ++ "Content-Length:10\r\n", ++ ""}, ++ Config, ++ [{statuscode, 400}]), ++ ++ %% Test 2: Content-Length before Transfer-Encoding ++ ok = http_status(Request, ++ {"Content-Length:10\r\n" ++ "Transfer-Encoding:chunked\r\n", ++ ""}, ++ Config, ++ [{statuscode, 400}]), ++ ++ %% Test 3: Transfer-Encoding alone is fine ++ ok = http_status(Request, ++ {"Transfer-Encoding:chunked\r\n", ++ "5\r\nhello\r\n0\r\n\r\n"}, ++ Config, ++ [{statuscode, 200}]), ++ ++ %% Test 4: Content-Length alone is fine ++ ok = http_status(Request, ++ {"Content-Length:5\r\n", ++ "hello"}, ++ Config, ++ [{statuscode, 200}]), ++ ok. ++%%------------------------------------------------------------------------- + max_clients_1_1() -> + [{doc, "Test max clients limit"}]. + diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-74835-1.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-74835-1.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-74835-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-74835-1.patch 2026-10-19 10:49:59.000000000 +0000 @@ -0,0 +1,264 @@ +From: Lukas Backström +Date: Tue, 2 Jun 2026 10:41:36 +0200 +Subject: inets: Fix max_body_size to apply to chunks as we receive them + +Before this fix we only checked the max body size when a complete +chunk was received. Now we do a sanity check of the size first +and then we also check the data we receive so that a client that +sends more data than is allowed is stopped. + +Origin: upstream, https://github.com/erlang/otp/commit/0bceff0c2987cae83d9d4a77c5ecacd6d01b8b86 +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/inets/src/http_lib/http_chunk.erl | 39 +++++++++------- + .../src/http_server/httpd_request_handler.erl | 29 ++++++++---- + lib/inets/test/http_format_SUITE.erl | 2 +- + lib/inets/test/httpd_basic_SUITE.erl | 49 ++++++++++++++++++++ + 4 files changed, 92 insertions(+), 27 deletions(-) + +--- a/lib/inets/src/http_lib/http_chunk.erl ++++ b/lib/inets/src/http_lib/http_chunk.erl +@@ -58,8 +58,7 @@ + %%------------------------------------------------------------------------- + decode(ChunkedBody, MaxBodySize, MaxHeaderSize) -> + %% Note decode_size will call decode_data. +- decode_size([ChunkedBody, <<>>, [], 0, +- {MaxBodySize, <<>>, 0, MaxHeaderSize}]). ++ decode_size([ChunkedBody, <<>>, [], 0, {MaxBodySize, <<>>, 0, MaxHeaderSize}]). + + %%------------------------------------------------------------------------- + %% encode(Chunk) -> EncodedChunk +@@ -135,6 +134,7 @@ ignore_extensions([Bin, Rest, RemainingSize, TotalMaxHeaderSize, NextFunction]) + decode_data([Bin, ChunkSize, TotalChunk, Info]) -> + decode_data(ChunkSize, <>, Info). + ++ + decode_trailer([Bin, Rest, Header, Headers, Body, + BodyLength, RemainingSize, TotalMaxHeaderSize]) -> + decode_trailer(<>, +@@ -145,8 +145,7 @@ decode_trailer([Bin, Rest, Header, Headers, Body, + %%%======================================================================== + decode_size(_, _, AccHeaderSize, {_,_,_, MaxHeaderSize}) when + AccHeaderSize > MaxHeaderSize -> +- throw({error, {header_too_long, {max, MaxHeaderSize}}}); +- ++ throw_error(header_too_long, MaxHeaderSize); + decode_size(<<>>, HexList, AccHeaderSize, Info) -> + {?MODULE, decode_size, [<<>>, HexList, AccHeaderSize, Info]}; + decode_size(Data = <>, HexList, AccHeaderSize, +@@ -160,6 +159,8 @@ decode_size(Data = <>, HexList, AccHeaderSize, + [<<>>, [],[], + Body, + integer_to_list(AccLength)]}); ++ ChunkSize when AccLength + ChunkSize > MaxBodySize -> ++ throw_error(body_too_long, MaxBodySize); + ChunkSize -> + %% Note decode_data may call decode_size again if there + %% is more than one chunk, hence here is where the last parameter +@@ -169,7 +170,7 @@ decode_size(Data = <>, HexList, AccHeaderSize, + MaxHeaderSize}) + catch + _:_ -> +- throw({error, {chunk_size, lists:reverse(HexList)}}) ++ throw_error(chunk_size, lists:reverse(HexList)) + end; + decode_size(<<";", Rest/binary>>, HexList, AccHeaderSize, {_,_,_, MaxHeaderSize} = Info) -> + %% Note ignore_extensions will call decode_size/1 again when +@@ -185,7 +186,7 @@ decode_size(<>, HexList, AccHeaderSize, Info) -> + %% do not understand.", see RFC 2616 Section 3.6.1 We don't + %% understand any extension... + ignore_extensions(_, 0, TotalMaxHeaderSize, _) -> +- throw({error, {header_too_long, {max, TotalMaxHeaderSize}}}); ++ throw_error(header_too_long, TotalMaxHeaderSize); + ignore_extensions(<<>>, RemainingSize, TotalMaxHeaderSize, NextFunction) -> + {?MODULE, ignore_extensions, [<<>>, RemainingSize, TotalMaxHeaderSize, NextFunction]}; + ignore_extensions(Data = <>, RemainingSize, TotalMaxHeaderSize, +@@ -201,9 +202,8 @@ ignore_extensions(<> = Data, RemainingSize, TotalMaxHeaderSize, NextFunctio + ignore_extensions(<<_Octet, Rest/binary>>, RemainingSize, TotalMaxHeaderSize, NextFunction) -> + ignore_extensions(Rest, remaing_size(RemainingSize, 1), TotalMaxHeaderSize, NextFunction). + +-decode_data(ChunkSize, TotalChunk, +- Info = {MaxBodySize, BodySoFar, AccLength, MaxHeaderSize}) +- when is_binary(TotalChunk), ChunkSize =< byte_size(TotalChunk) -> ++decode_data(ChunkSize, TotalChunk, Info = {MaxBodySize, BodySoFar, AccLength, MaxHeaderSize}) ++ when is_binary(TotalChunk), ChunkSize =< byte_size(TotalChunk) -> + case TotalChunk of + %% Last chunk + <> -> +@@ -231,24 +231,22 @@ decode_data(ChunkSize, TotalChunk, + <>, + integer_to_list(AccLength), MaxHeaderSize, MaxHeaderSize); + %% There are more chunks, so here we go again... +- <> -> +- NewBody = <>, +- {?MODULE, decode_size, [<<>>, [], 0, {MaxBodySize, NewBody, AccLength, MaxHeaderSize}]}; +- <> +- when (AccLength < MaxBodySize) or (MaxBodySize == nolimit) -> ++ <> -> + decode_size(Rest, [], 0, + {MaxBodySize, <>, + AccLength, MaxHeaderSize}); +- <<_:ChunkSize/binary, ?CR, ?LF, _/binary>> -> +- throw({error, {body_too_big, {max, MaxBodySize}}}); ++ <<_:(ChunkSize)/binary, Rest/binary>> ++ when MaxBodySize =/= nolimit, byte_size(Rest) + ChunkSize + byte_size(BodySoFar) > MaxBodySize -> ++ throw_error(body_too_long, MaxBodySize); + _ -> ++ %% Need more data + {?MODULE, decode_data, [ChunkSize, TotalChunk, Info]} +- end; ++ end; + decode_data(ChunkSize, TotalChunk, Info) -> + {?MODULE, decode_data, [ChunkSize, TotalChunk, Info]}. + + decode_trailer(_,_,_,_,_, 0, TotalMaxHeaderSize) -> +- throw({error, {header_too_long, {max, TotalMaxHeaderSize}}}); ++ throw_error(header_too_long, TotalMaxHeaderSize); + decode_trailer(<<>>, Header, Headers, Body, BodyLength, RemainingSize, TotalMaxHeaderSize) -> + {?MODULE, decode_trailer, [<<>>, Header, Headers, Body, + BodyLength, RemainingSize, TotalMaxHeaderSize]}; +@@ -297,3 +295,8 @@ encode_trailers([], Acc) -> + Acc ++ ?CRLF ++ ?CRLF; + encode_trailers([{Header, Value} | Rest], Acc) -> + encode_trailers(Rest, Header ++ ":" ++ Value ++ ?CRLF ++ Acc). ++ ++throw_error(What, MaxSize) when What =:= header_too_long; What =:= body_too_long -> ++ throw({error, {What, {max, MaxSize}}}); ++throw_error(What, ChunkSize) when What =:= chunk_size -> ++ throw({error, {chunk_size, ChunkSize}}). +--- a/lib/inets/src/http_server/httpd_request_handler.erl ++++ b/lib/inets/src/http_server/httpd_request_handler.erl +@@ -255,6 +255,7 @@ handle_info({Proto, Socket, Data}, + end, + handle_msg(Result, NewState); + ++ %% error returns from httpd_request + {error, {size_error, MaxSize, ErrCode, ErrStr}, Version} -> + NewModData = ModData#mod{http_version = Version}, + httpd_response:send_status(NewModData, ErrCode, ErrStr, {max_size, MaxSize}), +@@ -285,9 +286,7 @@ handle_info({Proto, Socket, Data}, + {noreply, State#state{mfa = NewMFA, data = NewDataSize}} + end + catch throw:{error, Error} when Module =:= http_chunk -> +- httpd_response:send_status(ModData, 400, +- "Bad input", {chunk_decoding, bad_input, Error}), +- {stop, normal, State#state{response_sent = true}} ++ handle_chunk_size_error(Error, State) + end; + + %% Error cases +@@ -522,12 +521,13 @@ handle_body(#state{headers = Headers, body = Body, + NewHeaders = http_chunk:handle_headers(Headers, ChunkedHeaders), + handle_response(State#state{headers = NewHeaders, + body = NewBody, +- chunk = chunk_finish(ChunkState, CbState, MaxChunk)}) ++ chunk = chunk_finish(ChunkState, CbState, MaxChunk)}); ++ {error, {size_error, MaxSize, ErrCode, ErrStr}} -> ++ httpd_response:send_status(ModData, ErrCode, ErrStr, {max_size, MaxSize}), ++ {stop, normal, State#state{response_sent = true}} + catch +- throw:Error -> +- httpd_response:send_status(ModData, 400, +- "Bad input", {chunk_decoding, bad_input, Error}), +- {stop, normal, State#state{response_sent = true}} ++ throw:{error, Error} -> ++ handle_chunk_size_error(Error, State) + end; + Encoding when is_list(Encoding) -> + httpd_response:send_status(ModData, 501, +@@ -568,6 +568,19 @@ handle_body(#state{headers = Headers, body = Body, + end + end. + ++handle_chunk_size_error({chunk_size, _} = Error, State = #state{mod = ModData}) -> ++ httpd_response:send_status(ModData, 400, ++ "Bad input", {chunk_decoding, bad_input, Error}), ++ {stop, normal, State#state{response_sent = true}}; ++handle_chunk_size_error({header_too_long, _} = Error, State = #state{mod = ModData}) -> ++ httpd_response:send_status(ModData, 413, ++ "Header too long", {chunk_decoding, bad_input, Error}), ++ {stop, normal, State#state{response_sent = true}}; ++handle_chunk_size_error({body_too_long, _} = Error, State = #state{mod = ModData}) -> ++ httpd_response:send_status(ModData, 413, ++ "Body too long", {chunk_decoding, bad_input, Error}), ++ {stop, normal, State#state{response_sent = true}}. ++ + handle_expect(#state{headers = Headers, mod = + #mod{config_db = ConfigDB} = ModData} = State, + MaxBodySize) -> +--- a/lib/inets/test/http_format_SUITE.erl ++++ b/lib/inets/test/http_format_SUITE.erl +@@ -291,7 +291,7 @@ chunk_max_bodysize(Config) when is_list(Config) -> + ?HTTP_MAX_BODY_SIZE, ?HTTP_MAX_HEADER_SIZE), + + %% Too long body +- {error,{body_too_big, {max, 10}}} = ++ {error,{body_too_long, {max, 10}}} = + (catch http_chunk:decode(list_to_binary(ChunkedBody), + 10, ?HTTP_MAX_HEADER_SIZE)). + +--- a/lib/inets/test/httpd_basic_SUITE.erl ++++ b/lib/inets/test/httpd_basic_SUITE.erl +@@ -45,6 +45,7 @@ groups() -> + script_nocache, + escaped_url_in_error_body, + script_timeout, ++ chunked_body_size_unbounded, + slowdose, + keep_alive_timeout, + invalid_rfc1123_date]}]. +@@ -403,6 +404,54 @@ invalid_rfc1123_date(Config) when is_list(Config) -> + Rfc1123FormattedDate =:= httpd_util:rfc1123_date(NonDSTDateTime). + + ++%%------------------------------------------------------------------------- ++ ++chunked_body_size_unbounded() -> ++ [{doc, "check that never-completing and malformed chunks don't bypass max_body_size"}]. ++chunked_body_size_unbounded(Config) when is_list(Config) -> ++ MaxBody = 1024, ++ HttpdConf = proplists:get_value(httpd_conf, Config), ++ {ok, Pid} = inets:start(httpd, [{port, 0}, {max_body_size, MaxBody} | HttpdConf]), ++ Port = proplists:get_value(port, httpd:info(Pid)), ++ try ++ %% Control: completed chunk > max_body_size -> hits ?CR?LF -> body_too_big. ++ chunk_probe(Port, [io_lib:format("~.16b\r\n", [4 * MaxBody]), ++ payload(4 * MaxBody), "\r\n0\r\n\r\n"]), ++ %% (a) huge declared size, stream 64x the limit, never terminate. ++ chunk_probe(Port, ["FFFFFFFF\r\n", payload(64 * MaxBody)]), ++ %% (b) correct size, non-CRLF where the terminator belongs, keep streaming. ++ chunk_probe(Port, ["5\r\nAAAAA", "X", payload(64 * MaxBody)]), ++ %% (c) many small chunks that together exceed the limit, never terminate. ++ chunk_probe(Port, lists:flatten([iolist_to_binary([io_lib:format("~.16b\r\n", [MaxBody div 10]), ++ payload(MaxBody div 10), "\r\n"]) || _ <- lists:seq(1, 11)])) ++ ++ after ++ inets:stop(httpd, Pid) ++ end. ++ ++%% Open a chunked POST, send Body ++chunk_probe(Port, Body) -> ++ {ok, S} = inets_test_lib:connect_bin(ip_comm, "localhost", Port, []), ++ Req = ["POST / HTTP/1.1\r\n", ++ "Host: localhost\r\n", ++ "Transfer-Encoding: chunked\r\n\r\n"], ++ ok = inets_test_lib:send(ip_comm, S, iolist_to_binary(Req)), ++ [timer:sleep(50), inets_test_lib:send(ip_comm, S, iolist_to_binary(B)) || B <- Body], ++ Verdict = ++ receive ++ {tcp, S, Data} -> ++ case binary:match(Data, <<"413">>) of ++ {_, _} -> bounded; %% limit hit, server replied ++ nomatch -> ct:fail({"Server replied but did not indicate body too large", Data}) ++ end ++ after 5000 -> ++ ct:fail("Server did not reply within timeout, likely still buffering the body (bug)") ++ end, ++ inets_test_lib:close(ip_comm, S), ++ Verdict. ++ ++payload(N) -> binary:copy(<<$A>>, N). ++ + %%------------------------------------------------------------------------- + %% Internal functions + %%------------------------------------------------------------------------- diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-74835-2.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-74835-2.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-74835-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-74835-2.patch 2026-09-30 10:13:04.000000000 +0000 @@ -0,0 +1,21 @@ +From: Konrad Pietrzak +Date: Mon, 17 Aug 2026 12:23:20 +0200 +Subject: Fix httpd_basic_SUITE:chunked_body_size_unbounded/1 testcase for patch-base-27 + +Origin: upstream, https://github.com/erlang/otp/commit/faa928205bdf20d533e68720eac003a43d3595f0 +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/inets/test/httpd_basic_SUITE.erl | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- a/lib/inets/test/httpd_basic_SUITE.erl ++++ b/lib/inets/test/httpd_basic_SUITE.erl +@@ -450,7 +450,7 @@ chunk_probe(Port, Body) -> + "Host: localhost\r\n", + "Transfer-Encoding: chunked\r\n\r\n"], + ok = inets_test_lib:send(ip_comm, S, iolist_to_binary(Req)), +- [timer:sleep(50), inets_test_lib:send(ip_comm, S, iolist_to_binary(B)) || B <- Body], ++ [inets_test_lib:send(ip_comm, S, iolist_to_binary(B)) || B <- Body, is_atom(timer:sleep(50))], + Verdict = + receive + {tcp, S, Data} -> diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-74994.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-74994.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-74994.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-74994.patch 2026-10-19 10:50:19.000000000 +0000 @@ -0,0 +1,158 @@ +From: Konrad Pietrzak +Date: Fri, 24 Jul 2026 12:42:33 +0200 +Subject: inets: Fix per-directory auth namespace isolation for dets/mnesia + + The mod_auth dets and mnesia backends used a key that did not include + the directory path, causing all user/group records to collapse into a + single per-listener namespace. A user added to one protected directory + could authenticate against any other protected directory on the same + server. + + The root cause was that {path, Directory} was never included in + DirData when passed to the auth backend modules. Both the HTTP request + authentication path (secretp/2) and the admin API path + (mod_auth_server) now inject {path, Directory} into DirData before + calling the backend, ensuring that dets/mnesia keys are scoped + per-directory as documented. + + Add auth_directory_isolation test case to verify cross-directory + access is denied for both dets and mnesia backends. + +Origin: upstream, https://github.com/erlang/otp/commit/6982e381137ede21a4e1faf5fa2dd82321691176 +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/inets/src/http_server/mod_auth.erl | 12 ++-- + lib/inets/src/http_server/mod_auth_server.erl | 3 +- + lib/inets/test/httpd_SUITE.erl | 74 +++++++++++++++++++- + 3 files changed, 81 insertions(+), 8 deletions(-) + +--- a/lib/inets/src/http_server/mod_auth.erl ++++ b/lib/inets/src/http_server/mod_auth.erl +@@ -727,8 +727,9 @@ group_accepted(Info, User, [Group|Rest], Dir, DirData) -> + false + end. + +-check_password(User, Password, _Dir, DirData) -> +- case int_get_user(DirData, User) of ++check_password(User, Password, Dir, DirData) -> ++ DirDataWithPath = [{path, Dir} | DirData], ++ case int_get_user(DirDataWithPath, User) of + {ok, UStruct} -> + case UStruct#httpd_user.password of + Password -> +@@ -762,9 +763,10 @@ secretp(Path,ConfigDB) -> + Directories = ets:match(ConfigDB,{directory, {'$1','_'}}), + case secret_path(Path, Directories) of + {yes,Directory} -> +- {yes, {Directory, +- lists:flatten( +- ets:match(ConfigDB,{directory, {Directory,'$1'}}))}}; ++ DirData = lists:flatten( ++ ets:match(ConfigDB, {directory, {Directory, '$1'}})), ++ DirDataWithPath = [{path, Directory} | DirData], ++ {yes, {Directory, DirDataWithPath}}; + no -> + no + end. +--- a/lib/inets/src/http_server/mod_auth_server.erl ++++ b/lib/inets/src/http_server/mod_auth_server.erl +@@ -259,7 +259,8 @@ api_call(Addr, Port, Profile, Dir, Func, Args,Password,State) -> + case ets:match_object(ConfigName, {directory, {Dir, '$1'}}) of + [{directory, {Dir, DirData}}] -> + AuthMod = auth_mod_name(DirData), +- (catch apply(AuthMod, Func, [DirData|Args])); ++ DirDataWithPath = [{path, Dir} | DirData], ++ (catch apply(AuthMod, Func, [DirDataWithPath|Args])); + _ -> + {error, no_such_directory} + end; +--- a/lib/inets/test/httpd_SUITE.erl ++++ b/lib/inets/test/httpd_SUITE.erl +@@ -136,8 +136,8 @@ groups() -> + double_slash_auth_bypass, case_insensitive_auth_bypass, + auth_path_canonicalization]}, + {auth_api, [], [auth_api_1_1, auth_api_1_0]}, +- {auth_api_dets, [], [auth_api_1_1, auth_api_1_0]}, +- {auth_api_mnesia, [], [auth_api_1_1, auth_api_1_0]}, ++ {auth_api_dets, [], [auth_api_1_1, auth_api_1_0, auth_directory_isolation]}, ++ {auth_api_mnesia, [], [auth_api_1_1, auth_api_1_0, auth_directory_isolation]}, + {security, [], [security_1_1, security_1_0]}, + {logging, [], [disk_log_internal, disk_log_exists, + disk_log_bad_size, disk_log_bad_file]}, +@@ -901,6 +901,76 @@ ipv6(Config) when is_list(Config) -> + {skip, "Host does not support IPv6"} + end. + ++%%------------------------------------------------------------------------- ++auth_directory_isolation() -> ++ [{doc, "Verify that per-directory auth namespaces are isolated (GH-1052). " ++ "A user added to /dirA must NOT be able to authenticate against /dirB."}]. ++ ++auth_directory_isolation(Config) when is_list(Config) -> ++ Version = proplists:get_value(http_version, Config, "HTTP/1.1"), ++ Host = proplists:get_value(host, Config), ++ Port = proplists:get_value(port, Config), ++ Node = proplists:get_value(node, Config), ++ ServerRoot = proplists:get_value(server_root, Config), ++ Prefix = proplists:get_value(auth_prefix, Config), ++ ++ %% Directories: open requires user "one" or "Aladdin" ++ %% secret requires group "group1" or "group2" ++ OpenDir = Prefix ++ "open", ++ SecretDir = Prefix ++ "secret", ++ ++ %% Clean slate ++ remove_users(Node, ServerRoot, Host, Port, Prefix, "open"), ++ remove_users(Node, ServerRoot, Host, Port, Prefix, "secret"), ++ ++ %% Add user "one" to the "open" directory ++ true = add_user(Node, ServerRoot, Port, Prefix, "open", ++ "one", "onePassword", []), ++ ++ %% Add user "two" to the "secret" directory and add to group1 ++ true = add_user(Node, ServerRoot, Port, Prefix, "secret", ++ "two", "twoPassword", []), ++ true = add_group_member(Node, ServerRoot, Port, Prefix, "secret", ++ "two", "group1"), ++ ++ %% Test 1: "one" can access /open (require_user includes "one") ++ ok = auth_status(auth_request("/" ++ OpenDir ++ "/", ++ "one", "onePassword", Version, Host), ++ Config, [{statuscode, 200}]), ++ ++ %% Test 2: "two" can access /secret (in group1) ++ ok = auth_status(auth_request("/" ++ SecretDir ++ "/", ++ "two", "twoPassword", Version, Host), ++ Config, [{statuscode, 200}]), ++ ++ %% Test 3: KEY TEST — "one" must NOT access /secret ++ %% Before fix: 200 (namespace collapsed). After fix: 401. ++ ok = auth_status(auth_request("/" ++ SecretDir ++ "/", ++ "one", "onePassword", Version, Host), ++ Config, [{statuscode, 401}]), ++ ++ %% Test 4: "two" must NOT access /open ++ ok = auth_status(auth_request("/" ++ OpenDir ++ "/", ++ "two", "twoPassword", Version, Host), ++ Config, [{statuscode, 401}]), ++ ++ %% Test 5: get_user for "one" in /secret returns not found ++ SecretDirectory = filename:join([ServerRoot, "htdocs", SecretDir]), ++ {error, no_such_user} = ++ rpc:call(Node, mod_auth, get_user, ++ ["one", undefined, Port, SecretDirectory]), ++ ++ %% Test 6: get_user for "two" in /open returns not found ++ OpenDirectory = filename:join([ServerRoot, "htdocs", OpenDir]), ++ {error, no_such_user} = ++ rpc:call(Node, mod_auth, get_user, ++ ["two", undefined, Port, OpenDirectory]), ++ ++ %% Cleanup ++ remove_users(Node, ServerRoot, Host, Port, Prefix, "open"), ++ remove_users(Node, ServerRoot, Host, Port, Prefix, "secret"), ++ ok. ++ + %%------------------------------------------------------------------------- + same_file_name_dir_name() -> + [{doc,"Test that URI path that has a filename in it is not interpreted as the file"}]. diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-75538-1.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-75538-1.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-75538-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-75538-1.patch 2026-09-30 10:13:04.000000000 +0000 @@ -0,0 +1,52 @@ +From: Raimo Niskanen +Date: Fri, 21 Aug 2026 17:22:41 +0200 +Subject: Avoid signed int overflows + +Origin: upstream, https://github.com/erlang/otp/commit/08e8efdba8500d2d6f54c6b1de1492b228017c9b +Bug-Debian: https://bugs.debian.org/1146603 +--- + erts/emulator/beam/packet_parser.c | 16 ++++++++-------- + erts/emulator/drivers/common/inet_drv.c | 7 ++++++- + 2 files changed, 14 insertions(+), 9 deletions(-) + +--- a/erts/emulator/beam/packet_parser.c ++++ b/erts/emulator/beam/packet_parser.c +@@ -481,14 +481,14 @@ more: + return 0; + + remain: +- { +- int tlen = hlen + plen; +- if ((max_plen != 0 && plen > max_plen) +- || tlen < (int)hlen) { /* wrap-around protection */ +- return -1; +- } +- return tlen; +- } ++ ASSERT(INT_MAX >= hlen); ++ if (max_plen == 0) { ++ max_plen = INT_MAX - hlen; ++ } ++ if (plen > max_plen) { ++ return -1; ++ } ++ return hlen + plen; + + done: + return plen; +--- a/erts/emulator/drivers/common/inet_drv.c ++++ b/erts/emulator/drivers/common/inet_drv.c +@@ -11495,7 +11495,12 @@ static int tcp_expand_buffer(tcp_descriptor* desc, int len) + int offs1; + int offs2; + int used = desc->i_ptr_start - desc->i_buf->orig_bytes; +- int ulen = used + len; ++ int ulen; ++ ++ if (len > INT_MAX - used) { ++ return -1; ++ } ++ ulen = used + len; + + if (desc->i_bufsz >= ulen) /* packet will fit */ + return 0; diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-75538-2.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-75538-2.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-75538-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-75538-2.patch 2026-09-30 10:13:04.000000000 +0000 @@ -0,0 +1,123 @@ +From: Raimo Niskanen +Date: Wed, 26 Aug 2026 10:30:31 +0200 +Subject: Test `{packet,4}` signed int overflow and memory corruption + +Origin: upstream, https://github.com/erlang/otp/commit/6432aeb0966652b12e6e7ee56b0fb5b1511e426d +Bug-Debian: https://bugs.debian.org/1146603 +--- + lib/kernel/test/gen_tcp_misc_SUITE.erl | 90 +++++++++++++++++++- + 1 file changed, 88 insertions(+), 2 deletions(-) + +--- a/lib/kernel/test/gen_tcp_misc_SUITE.erl ++++ b/lib/kernel/test/gen_tcp_misc_SUITE.erl +@@ -109,7 +109,8 @@ + otp_18883/1, + otp_18707/1, + otp_19560_inet/1, otp_19560_inet6/1, +- send_block_unblock/1 ++ send_block_unblock/1, ++ cve_2026_75538/1 + ]). + + %% Internal exports. +@@ -239,7 +240,8 @@ all_std_cases() -> + {group, socket_monitor}, + otp_17492, + otp_18707, +- send_block_unblock ++ send_block_unblock, ++ cve_2026_75538 + ]. + + ticket_cases() -> +@@ -9827,6 +9829,90 @@ payload(N, Bin) -> + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + ++cve_2026_75538(Config) when is_list(Config) -> ++ %% ++ %% Test in a peer node since when it fails the node crashes ++ %% ++ {ok, Peer, Node} = ++ test_server:start_peer([], ?MODULE, ?FUNCTION_NAME), ++ ++ try ++ _ = process_flag(trap_exit, true), ++ ++ Ref = make_ref(), ++ Pid = spawn_link(Node, fun () -> exit({Ref,cve_2026_75538_test()}) end), ++ receive ++ {'EXIT', Pid, {Ref, ok}} -> ++ ok; ++ {'EXIT', Pid, Other} -> ++ error({'EXIT', Other}) ++ end, ++ ++ case flush([]) of ++ [] -> ++ ok; ++ Garbage -> ++ error({garbage, Garbage}) ++ end ++ ++ after ++ try peer:stop(Peer) ++ catch _ : _ -> ok ++ end ++ end. ++ ++cve_2026_75538_test() -> ++ %% Buffer overflow in inet_drv for {packet,4} ++ %% when receiving a packet of size just below INT_MAX ++ ++ %% A 1 KB binary ++ BinK = binary:copy(<<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>>, 16*4), ++ %% A 16 MB binary ++ Bulk = binary:copy(BinK, 16 bsl 10), ++ ++ %% A packet of size 4 followed by an attack packet ++ Data = <<4:32, 1,2,3,4, 16#7FFF_FFFB:32, Bulk/binary>>, ++ %% ++ %% The first small packet lingers in the receive buffer, ++ %% and the calculation in tcp_expand_buffer with the resulting ++ %% length 16#7FFF_FFFF (16#7FFF_FFFB + 4 (header size)) ++ %% plus the lingering packet size 5 will overflow to a negative ++ %% number which is less than the currently allocated size, ++ %% so the buffer is not expanded. ++ %% ++ %% After this the Bulk data is received into the small initial buffer ++ %% and destroys allocator metadata and subsequent blocks. ++ %% ++ %% The Bulk Data should be large enough to overwrite most of the VM ++ %% after the inet_drv buffer, so it should crash. ++ ++ Parent = self(), ++ _ = spawn_link( % Watchdog ++ fun () -> ++ receive ++ after 20_000 -> ++ exit(Parent, timeout) ++ end ++ end), ++ {ok, L} = gen_tcp:listen(0, [{packet,4}, {active, true}]), ++ {ok, Port} = inet:port(L), ++ {ok, C} = gen_tcp:connect({127,0,0,1}, Port, []), ++ {ok, A} = gen_tcp:accept(L), ++ ok = gen_tcp:close(L), ++ ok = gen_tcp:send(C, Data), ++ gen_tcp:close(C), ++ receive {tcp, A, [1,2,3,4]} -> ok end, ++ receive {tcp_error, A, emsgsize} -> ok end, ++ receive {tcp_closed, A} -> ok end, ++ gen_tcp:close(A), ++ case flush([]) of ++ [] -> ok; ++ Garbage -> ++ exit({peer_garbage, Garbage}) ++ end. ++ ++%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ++ + is_windows() -> + case os:type() of + {win32, nt} -> diff -Nru erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-89422.patch erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-89422.patch --- erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-89422.patch 1970-01-01 00:00:00.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/CVE-2026-89422.patch 2026-09-30 10:13:04.000000000 +0000 @@ -0,0 +1,359 @@ +From: Ingela Anderton Andin +Date: Wed, 16 Sep 2026 14:55:28 +0200 +Subject: ssl: Reject unsolicited TLS-1.3 pre_shared_key in client + +A TLS-1.3 client that offered no PSK accepted a ServerHello carrying a +pre_shared_key extension: get_pre_shared_key/4 fell back to the all-zero +"no PSK" value and handle_resumption/2 set resumption=true on the mere +presence of the extension, routing the client past the certificate +states (wait_cert_cr/wait_cert/wait_cv) straight to wait_finished. Since +the handshake is keyed with the ordinary non-PSK schedule, any peer that +completes ECDHE could impersonate any server to a verify_peer client +without presenting a certificate (server-authentication bypass). + +Fix (RFC 8446 4.2.11): in get_pre_shared_key/4 every clause reachable +only when the server DID send pre_shared_key now aborts with a fatal +illegal_parameter alert (reason {unsolicited_pre_shared_key, _}) instead +of returning the zero PSK - the client offered no identities, so +selected_identity cannot be in range. The auto clause still unlocks +tickets first. This alone closes the hole: handle_server_hello/2 fails +in wait_sh before any keys are derived. + +Defence in depth: move the handle_resumption/2 call in +handle_server_hello/2 to after get_pre_shared_key/4 has accepted the +server's PSK selection, so resumption mode can never be entered for a +PSK the client did not offer. + +Add regression test tls13_reject_unsolicited_psk (rogue_server_tests +group in tls_api_SUITE): a certificate-less raw-TCP TLS-1.3 server sends +an otherwise-valid flight with an unsolicited pre_shared_key. Verified +the test fails (client connects) without the fix and passes +(illegal_parameter) with it. Covers session_tickets=disabled and =auto. + +Origin: upstream, https://github.com/erlang/otp/commit/fd1d9d07fc92ec0d59f96dfb66182195882bb7dd +--- + lib/ssl/src/tls_client_connection_1_3.erl | 23 ++- + lib/ssl/src/tls_handshake_1_3.erl | 26 +-- + lib/ssl/test/tls_api_SUITE.erl | 188 +++++++++++++++++++- + 3 files changed, 215 insertions(+), 22 deletions(-) + +--- a/lib/ssl/src/tls_client_connection_1_3.erl ++++ b/lib/ssl/src/tls_client_connection_1_3.erl +@@ -755,10 +755,6 @@ handle_server_hello(#server_hello{cipher_suite = SelectedCipherSuite, + %% Go to state 'start' if server replies with 'HelloRetryRequest'. + Maybe(tls_handshake_1_3:maybe_hello_retry_request(ServerHello, State0)), + +- %% Resumption and PSK +- State1 = tls_gen_connection_1_3:handle_resumption(State0, +- ServerPreSharedKey), +- + Maybe(validate_cipher_suite(SelectedCipherSuite, ClientCiphers)), + Maybe(validate_server_key_share(ClientGroups, ServerKeyShare)), + +@@ -770,7 +766,7 @@ handle_server_hello(#server_hello{cipher_suite = SelectedCipherSuite, + client_private_key(SelectedGroup, + ClientKeyShare#key_share_client_hello.client_shares), + %% Update state +- State2 = tls_handshake_1_3:update_start_state(State1, ++ State2 = tls_handshake_1_3:update_start_state(State0, + #{cipher => SelectedCipherSuite, + key_share => ClientKeyShare, + session_id => SessionId, +@@ -787,13 +783,22 @@ handle_server_hello(#server_hello{cipher_suite = SelectedCipherSuite, + UseTicket, + HKDFAlgo, + ServerPreSharedKey)), +- State3 = ++ ++ %% Resumption and PSK. Only enter resumption mode once the server's PSK ++ %% selection has been accepted by get_pre_shared_key/4 above; an ++ %% unsolicited pre_shared_key aborts before this point, so the ++ %% resumption flag (which skips the certificate states) can never be ++ %% set for a PSK the client did not offer. Defence in depth on top of ++ %% the get_pre_shared_key/4 check. ++ State3 = tls_gen_connection_1_3:handle_resumption(State2, ++ ServerPreSharedKey), ++ State4 = + tls_handshake_1_3:calculate_handshake_secrets(ServerPublicKey, + ClientPrivateKey, + SelectedGroup, +- PSK, State2), +- State4 = ssl_record:step_encryption_state_read(State3), +- {State4, wait_ee} ++ PSK, State3), ++ State5 = ssl_record:step_encryption_state_read(State4), ++ {State5, wait_ee} + catch + {Ref, {State, StateName, ServerHello}} -> + {State, StateName, ServerHello}; +--- a/lib/ssl/src/tls_handshake_1_3.erl ++++ b/lib/ssl/src/tls_handshake_1_3.erl +@@ -1056,28 +1056,32 @@ get_pre_shared_key({_, PSK}, _) -> + %% Server initiates a full handshake + get_pre_shared_key(_, _, HKDFAlgo, undefined) -> + {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))}; +-%% Session resumption not configured +-get_pre_shared_key(undefined, _, HKDFAlgo, _) -> +- {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))}; +-get_pre_shared_key(_, undefined, HKDFAlgo, _) -> +- {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))}; ++%% Session resumption not configured, i.e. we did not offer any PSK, so the ++%% server's selected_identity cannot be within the range we supplied. ++%% RFC 8446 Section 4.2.11: abort with an "illegal_parameter" alert instead of ++%% silently falling back to the (zero) "no PSK" value, which would skip server ++%% authentication. ++get_pre_shared_key(undefined, _, _, ServerPSK) -> ++ {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, {unsolicited_pre_shared_key, ServerPSK})}; ++get_pre_shared_key(_, undefined, _, ServerPSK) -> ++ {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, {unsolicited_pre_shared_key, ServerPSK})}; + %% Session resumption +-get_pre_shared_key(manual = SessionTickets, UseTicket, HKDFAlgo, ServerPSK) -> ++get_pre_shared_key(manual = SessionTickets, UseTicket, _HKDFAlgo, ServerPSK) -> + TicketData = get_ticket_data(self(), SessionTickets, UseTicket), + case choose_psk(TicketData, ServerPSK) of +- undefined -> %% full handshake, default PSK +- {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))}; ++ undefined -> %% No PSK was offered that matches the server selection ++ {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, {unsolicited_pre_shared_key, ServerPSK})}; + illegal_parameter -> + {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER)}; + {_, PSK, _, _, _} -> + {ok, PSK} + end; +-get_pre_shared_key(auto = SessionTickets, UseTicket, HKDFAlgo, ServerPSK) -> ++get_pre_shared_key(auto = SessionTickets, UseTicket, _HKDFAlgo, ServerPSK) -> + TicketData = get_ticket_data(self(), SessionTickets, UseTicket), + case choose_psk(TicketData, ServerPSK) of +- undefined -> %% full handshake, default PSK ++ undefined -> %% No PSK was offered that matches the server selection + tls_client_ticket_store:unlock_tickets(self(), UseTicket), +- {ok, binary:copy(<<0>>, ssl_cipher:hash_size(HKDFAlgo))}; ++ {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER, {unsolicited_pre_shared_key, ServerPSK})}; + illegal_parameter -> + tls_client_ticket_store:unlock_tickets(self(), UseTicket), + {error, ?ALERT_REC(?FATAL, ?ILLEGAL_PARAMETER)}; +--- a/lib/ssl/test/tls_api_SUITE.erl ++++ b/lib/ssl/test/tls_api_SUITE.erl +@@ -30,6 +30,7 @@ + -include_lib("ssl/src/tls_handshake.hrl"). + -include_lib("ssl/src/ssl_alert.hrl"). + -include_lib("ssl/src/ssl_cipher.hrl"). ++-include_lib("ssl/src/tls_handshake_1_3.hrl"). + + %% Common test + -export([all/0, +@@ -118,7 +119,9 @@ + tls_reject_unoffered_cipher_suite/0, + tls_reject_unoffered_cipher_suite/1, + tls_reject_unoffered_alpn/0, +- tls_reject_unoffered_alpn/1 ++ tls_reject_unoffered_alpn/1, ++ tls13_reject_unsolicited_psk/0, ++ tls13_reject_unsolicited_psk/1 + ]). + + %% Apply export +@@ -138,6 +141,15 @@ + %% 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). ++%% TLS 1.3 constants for the unsolicited-pre_shared_key rogue-server test. ++%% Only genuinely-new TLS-1.3 extension/group constants with no equivalent in ++%% the included headers are declared here; handshake/content-type constants ++%% reuse the header macros (?ENCRYPTED_EXTENSIONS, ?FINISHED, ?HANDSHAKE, ++%% ?APPLICATION_DATA). ++-define(TLS13_KEY_SHARE_EXT, 51). ++-define(TLS13_PRE_SHARED_KEY_EXT, 41). ++-define(TLS13_SUPPORTED_VERSIONS_EXT, 43). ++-define(TLS13_GROUP_X25519, 16#001d). + -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}). +@@ -206,7 +218,8 @@ api_tests() -> + + rogue_server_tests() -> + [tls_reject_unoffered_cipher_suite, +- tls_reject_unoffered_alpn]. ++ tls_reject_unoffered_alpn, ++ tls13_reject_unsolicited_psk]. + + init_per_suite(Config0) -> + catch crypto:stop(), +@@ -933,6 +946,177 @@ rogue_read_record(Sock) -> + {error, _} = E -> E + end. + ++%%-------------------------------------------------------------------- ++tls13_reject_unsolicited_psk() -> ++ [{doc, "A malicious/on-path server sends a complete, otherwise-valid " ++ "TLS-1.3 flight (ServerHello, ChangeCipherSpec, encrypted " ++ "{EncryptedExtensions, Finished}) that carries a pre_shared_key " ++ "extension the client never offered, and NO Certificate/CertificateVerify. " ++ "RFC 8446 4.2.11 requires the client to abort with illegal_parameter. On " ++ "vulnerable code the verify_peer client instead accepts the unsolicited " ++ "PSK, keys the handshake with the zero PSK, skips the certificate states " ++ "and connects -- a complete server-authentication bypass " ++ "(ANT-2026-HEF8F3FY / OTP-20388). Checked for session_tickets=disabled " ++ "(default) and =auto with an empty ticket store."}]. ++tls13_reject_unsolicited_psk(Config) when is_list(Config) -> ++ ClientOpts = ssl_test_lib:ssl_options(client_rsa_verify_opts, Config), ++ ok = rogue_tls13_psk(ClientOpts, []), ++ ok = rogue_tls13_psk(ClientOpts, [{session_tickets, auto}]), ++ ok. ++ ++%% Drive a real TLS-1.3 verify_peer client against a raw-TCP attacker that ++%% forges the full flight, and assert the mandated illegal_parameter alert. ++rogue_tls13_psk(ClientOpts0, ExtraOpts) -> ++ {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, rogue_tls13_server(LSock)} end), ++ ClientOpts = ExtraOpts ++ ++ [{versions, ['tlsv1.3']}, ++ {ciphers, ["TLS_AES_128_GCM_SHA256"]}, ++ {supported_groups, [x25519]}, ++ {server_name_indication, disable}, ++ {active, false} | ClientOpts0], ++ Result = ssl:connect("localhost", Port, ClientOpts, 5000), ++ gen_tcp:close(LSock), ++ Observed = receive {attacker, O} -> O after 5000 -> unlink(Attacker), timeout end, ++ ct:log("extra=~p~n ssl:connect -> ~p~n attacker -> ~p", ++ [ExtraOpts, Result, Observed]), ++ %% With a well-formed flight, a PATCHED client aborts with ++ %% illegal_parameter in wait_sh (the unsolicited-PSK check). A ++ %% VULNERABLE client instead accepts the unsolicited PSK, skips the ++ %% certificate states and returns {ok, Socket} (verified: without the ++ %% fix this case fails here with {ok,_}). The reason atom ++ %% {unsolicited_pre_shared_key,_} is not rendered into the tls_alert ++ %% description string, so we assert on the description + the fact that ++ %% a vulnerable client connects. ++ case Result of ++ {error, {tls_alert, {illegal_parameter, _}}} -> ++ ok; ++ {ok, Sock} -> ++ ssl:close(Sock), ++ ct:fail("SERVER-AUTHENTICATION BYPASS: verify_peer client connected " ++ "to a certificate-less server that sent an unsolicited " ++ "pre_shared_key (extra ~p). Attacker observed ~p.", ++ [ExtraOpts, Observed]); ++ Other -> ++ ct:fail("Expected illegal_parameter for unsolicited pre_shared_key " ++ "(extra ~p); got ~p (attacker ~p)", ++ [ExtraOpts, Other, Observed]) ++ end. ++ ++%% Raw-TCP rogue TLS-1.3 server. No certificate. Sends a valid ServerHello + ++%% CCS + encrypted {EncryptedExtensions, Finished} using the RFC 8446 7.1 key ++%% schedule so that an UNPATCHED client would complete the handshake. ++rogue_tls13_server(LSock) -> ++ {ok, Sock} = gen_tcp:accept(LSock, 5000), ++ {?HANDSHAKE, CHHandshake} = rogue_read_record(Sock), ++ ClientShare = rogue_tls13_client_x25519(CHHandshake), ++ SessionId = rogue_tls13_client_session_id(CHHandshake), ++ ++ {ServerPub, ServerPriv} = crypto:generate_key(ecdh, x25519), ++ SHBody = rogue_tls13_server_hello_body(ServerPub, SessionId), ++ SHHandshake = rogue_handshake(?SERVER_HELLO, SHBody), ++ ++ %% RFC 8446 7.1 handshake secrets (transcript = ClientHello ++ ServerHello). ++ Alg = sha256, ++ HashLen = 32, ++ Shared = crypto:compute_key(ecdh, ClientShare, ServerPriv, x25519), ++ Zero = binary:copy(<<0>>, HashLen), ++ EarlySecret = tls_v1:hkdf_extract(Alg, Zero, Zero), ++ DerivedEarly = rogue_derive_secret(Alg, HashLen, EarlySecret, <<"derived">>, <<>>), ++ HSSecret = tls_v1:hkdf_extract(Alg, DerivedEarly, Shared), ++ Transcript0 = <>, ++ ServerHS = rogue_derive_secret(Alg, HashLen, HSSecret, <<"s hs traffic">>, Transcript0), ++ Key = tls_v1:hkdf_expand_label(ServerHS, <<"key">>, <<>>, 16, Alg), ++ IV = tls_v1:hkdf_expand_label(ServerHS, <<"iv">>, <<>>, 12, Alg), ++ ++ EE = <>, ++ Transcript1 = <>, ++ FinKey = tls_v1:hkdf_expand_label(ServerHS, <<"finished">>, <<>>, HashLen, Alg), ++ THash = crypto:hash(Alg, Transcript1), ++ VerifyData = tls_v1:hmac_hash(Alg, FinKey, THash), ++ Finished = <>, ++ ++ Inner = <>, ++ EncRecord = rogue_tls13_encrypt(Key, IV, 0, Inner), ++ ++ ok = gen_tcp:send(Sock, rogue_record(?HANDSHAKE, SHHandshake)), ++ ok = gen_tcp:send(Sock, <<20, 3, 3, 1:16, 1>>), %% ChangeCipherSpec ++ ok = gen_tcp:send(Sock, EncRecord), ++ ++ Obs = rogue_observe(Sock), ++ gen_tcp:close(Sock), ++ Obs. ++ ++rogue_tls13_server_hello_body(ServerPub, SessionId) -> ++ Random = crypto:strong_rand_bytes(32), ++ SVExt = <>, ++ KSData = <>, ++ KSExt = <>, ++ PSKExt = <>, %% selected_identity = 0 ++ Exts = <>, ++ %% RFC 8446 4.1.3: the ServerHello MUST echo the client's ++ %% legacy_session_id, otherwise the client aborts with ++ %% session_id_echo_mismatch before the pre_shared_key is examined. ++ <<16#03, 16#03, Random/binary, ++ (byte_size(SessionId)), SessionId/binary, ++ 16#13, 16#01, 0, %% TLS_AES_128_GCM_SHA256, compression=null ++ (byte_size(Exts)):16, Exts/binary>>. ++ ++rogue_derive_secret(Alg, HashLen, Secret, Label, Messages) -> ++ Hash = crypto:hash(Alg, Messages), ++ tls_v1:hkdf_expand_label(Secret, Label, Hash, HashLen, Alg). ++ ++%% Encrypt one TLS-1.3 record (outer type application_data), RFC 8446 5.2. ++rogue_tls13_encrypt(Key, IV, SeqNo, Inner) -> ++ TagLen = 16, ++ Len = byte_size(Inner) + TagLen, ++ AAD = <>, ++ Nonce = rogue_tls13_nonce(SeqNo, IV), ++ {Enc, Tag} = crypto:crypto_one_time_aead(aes_128_gcm, Key, Nonce, Inner, AAD, TagLen, true), ++ Payload = <>, ++ <>. ++ ++rogue_tls13_nonce(SeqNo, IV) -> ++ Padded = <<0:((byte_size(IV) - 8) * 8), SeqNo:64>>, ++ crypto:exor(Padded, IV). ++ ++%% Parse client's legacy_session_id from the ClientHello handshake message. ++rogue_tls13_client_session_id(CHHandshake) -> ++ <<_HsType, _HsLen:24, Body/binary>> = CHHandshake, ++ <<_LegacyVsn:16, _Random:32/binary, SidLen, Sid:SidLen/binary, _/binary>> = Body, ++ Sid. ++ ++%% Parse client's x25519 key_share from the ClientHello handshake message. ++rogue_tls13_client_x25519(CHHandshake) -> ++ <<_HsType, _HsLen:24, Body/binary>> = CHHandshake, ++ <<_LegacyVsn:16, _Random:32/binary, R0/binary>> = Body, ++ <> = R0, ++ <<_Sid:SidLen/binary, R2/binary>> = R1, ++ <> = R2, ++ <<_Cs:CsLen/binary, R4/binary>> = R3, ++ <> = R4, ++ <<_Comp:CompLen/binary, R6/binary>> = R5, ++ <<_ExtsLen:16, Exts/binary>> = R6, ++ rogue_tls13_find_key_share(Exts). ++ ++rogue_tls13_find_key_share(<>) -> ++ <<_SharesLen:16, Shares/binary>> = ExtData, ++ rogue_tls13_find_x25519(Shares); ++rogue_tls13_find_key_share(<<_Type:16, ExtLen:16, _ExtData:ExtLen/binary, Rest/binary>>) -> ++ rogue_tls13_find_key_share(Rest); ++rogue_tls13_find_key_share(<<>>) -> ++ ct:fail(client_offered_no_key_share). ++ ++rogue_tls13_find_x25519(<>) -> ++ Ke; ++rogue_tls13_find_x25519(<<_Group:16, KeLen:16, _Ke:KeLen/binary, Rest/binary>>) -> ++ rogue_tls13_find_x25519(Rest); ++rogue_tls13_find_x25519(<<>>) -> ++ ct:fail(client_offered_no_x25519_share). ++ + %%-------------------------------------------------------------------- + tls_tcp_error_propagation_in_active_mode() -> + [{doc,"Test that process receives {ssl_error, Socket, closed} when tcp error ocurres"}]. 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-08-22 19:38:30.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/series 2026-10-20 16:55:29.000000000 +0000 @@ -42,4 +42,34 @@ CVE-2026-42792-2.patch CVE-2026-54890.patch CVE-2026-59251.patch -#ssh-test-macros.patch +ssh-test-macros.patch +CVE-2026-75538-1.patch +CVE-2026-75538-2.patch +CVE-2026-69664.patch +CVE-2026-74835-1.patch +CVE-2026-73270-1.patch +CVE-2026-70399.patch +CVE-2026-74994.patch +CVE-2026-66357.patch +CVE-2026-73812.patch +CVE-2026-74835-2.patch +CVE-2026-73276-1.patch +CVE-2026-73270-2.patch +CVE-2026-73276-2.patch +CVE-2026-71380-1.patch +CVE-2026-71380-2.patch +CVE-2026-71380-3.patch +CVE-2026-71380-4.patch +CVE-2026-71380-5.patch +CVE-2026-55951.patch +CVE-2026-59696.patch +CVE-2026-70409.patch +CVE-2026-70405.patch +CVE-2026-71380-6.patch +CVE-2026-65634.patch +CVE-2026-89422.patch +CVE-2026-68956-1.patch +CVE-2026-68956-2.patch +CVE-2026-68956-3.patch +CVE-2026-68956-4.patch +CVE-2026-70405-followup.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 2026-08-22 19:38:30.000000000 +0000 +++ erlang-27.3.4.1+dfsg/debian/patches/ssh-test-macros.patch 2026-10-19 04:27:04.000000000 +0000 @@ -1,5 +1,4 @@ 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 @@ -8,24 +7,13 @@ 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. +Origin: upstream, https://github.com/erlang/otp/commit/94776b53bed0ebfec163b20724a3f0e28f561a43 + https://github.com/erlang/otp/commit/981a866ece347e8355da2a561280f7d3f07eab52 +Reviewed-by: Aron Xu --- lib/ssh/test/ssh_sftpd_SUITE.erl | 1 + lib/ssh/test/ssh_test_lib.hrl | 2 ++