From c25e1844996111313e380e0c486e0b5e182ffb45 Mon Sep 17 00:00:00 2001 From: Azure Linux Security Servicing Account Date: Fri, 4 Sep 2026 09:19:30 +0000 Subject: [PATCH 1/3] Patch erlang for CVE-2026-75538, CVE-2026-74994, CVE-2026-74835, CVE-2026-73812, CVE-2026-73276, CVE-2026-73270, CVE-2026-71380, CVE-2026-70399, CVE-2026-69664, CVE-2026-66357, CVE-2026-55951 --- SPECS/erlang/CVE-2026-55951.patch | 666 ++++++++++++++++++++++++++++++ SPECS/erlang/CVE-2026-66357.patch | 91 ++++ SPECS/erlang/CVE-2026-69664.patch | 153 +++++++ SPECS/erlang/CVE-2026-70399.patch | 113 +++++ SPECS/erlang/CVE-2026-71380.patch | 169 ++++++++ SPECS/erlang/CVE-2026-73270.patch | 122 ++++++ SPECS/erlang/CVE-2026-73276.patch | 128 ++++++ SPECS/erlang/CVE-2026-73812.patch | 206 +++++++++ SPECS/erlang/CVE-2026-74835.patch | 277 +++++++++++++ SPECS/erlang/CVE-2026-74994.patch | 169 ++++++++ SPECS/erlang/CVE-2026-75538.patch | 60 +++ SPECS/erlang/erlang.spec | 16 +- 12 files changed, 2169 insertions(+), 1 deletion(-) create mode 100644 SPECS/erlang/CVE-2026-55951.patch create mode 100644 SPECS/erlang/CVE-2026-66357.patch create mode 100644 SPECS/erlang/CVE-2026-69664.patch create mode 100644 SPECS/erlang/CVE-2026-70399.patch create mode 100644 SPECS/erlang/CVE-2026-71380.patch create mode 100644 SPECS/erlang/CVE-2026-73270.patch create mode 100644 SPECS/erlang/CVE-2026-73276.patch create mode 100644 SPECS/erlang/CVE-2026-73812.patch create mode 100644 SPECS/erlang/CVE-2026-74835.patch create mode 100644 SPECS/erlang/CVE-2026-74994.patch create mode 100644 SPECS/erlang/CVE-2026-75538.patch diff --git a/SPECS/erlang/CVE-2026-55951.patch b/SPECS/erlang/CVE-2026-55951.patch new file mode 100644 index 00000000000..3a5826ee60f --- /dev/null +++ b/SPECS/erlang/CVE-2026-55951.patch @@ -0,0 +1,666 @@ +From dbc1c6cf20ae989d4508b9ba5284815b352b6862 Mon Sep 17 00:00:00 2001 +From: AllSpark +Date: Fri, 4 Sep 2026 09:08:55 +0000 +Subject: [PATCH] inets,stdlib,eldap: Bound integer parsing to mitigate DoS + +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: AI Backport of https://github.com/erlang/otp/commit/aba0fe8c2d700bf4ac94607cf7f00e53bbe4042d.patch +--- + lib/eldap/src/eldap.erl | 6 +- + lib/inets/src/http_client/httpc.erl | 52 ++++-- + 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 | 107 +++++++++++- + lib/snmp/src/misc/snmp_pdus.erl | 10 +- + lib/stdlib/src/uri_string.erl | 6 +- + 8 files changed, 268 insertions(+), 113 deletions(-) + +diff --git a/lib/eldap/src/eldap.erl b/lib/eldap/src/eldap.erl +index 7edd012..eea6689 100644 +--- a/lib/eldap/src/eldap.erl ++++ b/lib/eldap/src/eldap.erl +@@ -1422,10 +1422,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 --git a/lib/inets/src/http_client/httpc.erl b/lib/inets/src/http_client/httpc.erl +index 953f19b..23ca981 100644 +--- a/lib/inets/src/http_client/httpc.erl ++++ b/lib/inets/src/http_client/httpc.erl +@@ -156,7 +156,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, +@@ -165,6 +167,7 @@ request(Url, Profile) -> + | { ReceiverModule::atom() + , ReceiverFunction::atom() + , ReceiverArgs::list()}, ++ MaxSize :: non_neg_integer() | nolimit, + Result :: { StatusLine , [HttpHeader], HttpBodyResult} + | { StatusCode, HttpBodyResult} + | RequestId +@@ -218,7 +221,9 @@ request(Method, Request, HttpOptions, 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}, + StreamTo :: none | self | {self, once} | file:name_all(), + BodyFormat :: string | binary, + SocketOpt :: term(), +@@ -227,6 +232,7 @@ request(Method, Request, HttpOptions, Options) -> + | { ReceiverModule::atom() + , ReceiverFunction::atom() + , ReceiverArgs::list()}, ++ MaxSize :: non_neg_integer() | nolimit, + Profile :: atom() | pid(), + HttpVersion :: string(), + Result :: {StatusLine +@@ -927,7 +933,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(), +@@ -935,7 +943,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 +@@ -1120,16 +1129,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(), +@@ -1175,7 +1194,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(), +@@ -1184,7 +1205,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) -> +diff --git a/lib/inets/src/http_client/httpc_handler.erl b/lib/inets/src/http_client/httpc_handler.erl +index 1d91fe1..a10d3bd 100644 +--- a/lib/inets/src/http_client/httpc_handler.erl ++++ b/lib/inets/src/http_client/httpc_handler.erl +@@ -224,17 +224,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). +@@ -248,6 +254,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 -> +@@ -538,6 +558,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), +diff --git a/lib/inets/src/http_client/httpc_response.erl b/lib/inets/src/http_client/httpc_response.erl +index 88c828e..27e22b2 100644 +--- a/lib/inets/src/http_client/httpc_response.erl ++++ b/lib/inets/src/http_client/httpc_response.erl +@@ -56,9 +56,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 +@@ -195,7 +195,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); +@@ -245,7 +245,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 +@@ -259,100 +259,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: +diff --git a/lib/inets/src/http_lib/http_response.erl b/lib/inets/src/http_lib/http_response.erl +index cbdea82..2d616dd 100644 +--- a/lib/inets/src/http_lib/http_response.erl ++++ b/lib/inets/src/http_lib/http_response.erl +@@ -125,7 +125,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) -> +@@ -221,3 +224,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). +diff --git a/lib/inets/test/httpc_SUITE.erl b/lib/inets/test/httpc_SUITE.erl +index 720222f..a8b197b 100644 +--- a/lib/inets/test/httpc_SUITE.erl ++++ b/lib/inets/test/httpc_SUITE.erl +@@ -157,7 +157,7 @@ real_requests_esi() -> + + simulated_unix_socket() -> + [unix_domain_socket, +- invalid_ipfamily_unix_socket]. ++ invalid_ipfamily_unix_socket]. + + only_simulated() -> + [ +@@ -210,7 +210,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() -> +@@ -1396,7 +1399,7 @@ headers_conflict_chunked_with_length(doc) -> + 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, []), +- {error, {could_not_parse_as_http, _}} = httpc:request(get, Request, [{relaxed, false}, ?SSL_NO_VERIFY], RequestOpts), ++ {error, {headers_conflict, _}} = httpc:request(get, Request, [{relaxed, false}, ?SSL_NO_VERIFY], RequestOpts), + {ok,{{_,200,_},_,_}} = httpc:request(get, Request, [{relaxed, true}, ?SSL_NO_VERIFY], RequestOpts), + ok. + +@@ -2115,6 +2118,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," +diff --git a/lib/snmp/src/misc/snmp_pdus.erl b/lib/snmp/src/misc/snmp_pdus.erl +index 1967c70..3f8c352 100644 +--- a/lib/snmp/src/misc/snmp_pdus.erl ++++ b/lib/snmp/src/misc/snmp_pdus.erl +@@ -265,7 +265,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 +@@ -282,7 +282,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) -> +@@ -298,7 +298,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) -> +@@ -312,7 +312,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) -> +@@ -333,7 +333,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 --git a/lib/stdlib/src/uri_string.erl b/lib/stdlib/src/uri_string.erl +index 7d24a0d..c88ad98 100644 +--- a/lib/stdlib/src/uri_string.erl ++++ b/lib/stdlib/src/uri_string.erl +@@ -1364,12 +1364,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 +-- +2.45.4 + diff --git a/SPECS/erlang/CVE-2026-66357.patch b/SPECS/erlang/CVE-2026-66357.patch new file mode 100644 index 00000000000..fbc88146551 --- /dev/null +++ b/SPECS/erlang/CVE-2026-66357.patch @@ -0,0 +1,91 @@ +From 43ff2d8f9eb8eb2881072225a43276c0758ac187 Mon Sep 17 00:00:00 2001 +From: Konrad Pietrzak +Date: Mon, 27 Jul 2026 14:50:42 +0200 +Subject: [PATCH] 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. + +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/erlang/otp/commit/220d618d2479a7180b4a06d0c5aacfaa4af4a85b.patch +--- + lib/inets/src/http_server/httpd_request.erl | 5 ++++ + lib/inets/test/httpd_SUITE.erl | 27 +++++++++++++++++++++ + 2 files changed, 32 insertions(+) + +diff --git a/lib/inets/src/http_server/httpd_request.erl b/lib/inets/src/http_server/httpd_request.erl +index 3b5d90b..f687354 100644 +--- a/lib/inets/src/http_server/httpd_request.erl ++++ b/lib/inets/src/http_server/httpd_request.erl +@@ -253,6 +253,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 +diff --git a/lib/inets/test/httpd_SUITE.erl b/lib/inets/test/httpd_SUITE.erl +index fa8837a..38a5fd1 100644 +--- a/lib/inets/test/httpd_SUITE.erl ++++ b/lib/inets/test/httpd_SUITE.erl +@@ -175,6 +175,7 @@ http_get() -> + max_header, + max_content_length, + ignore_invalid_header, ++ reject_obs_fold, + ipv6, + same_file_name_dir_name + ]. +@@ -1638,6 +1639,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]). +-- +2.45.4 + diff --git a/SPECS/erlang/CVE-2026-69664.patch b/SPECS/erlang/CVE-2026-69664.patch new file mode 100644 index 00000000000..f0ee367dcf6 --- /dev/null +++ b/SPECS/erlang/CVE-2026-69664.patch @@ -0,0 +1,153 @@ +From 0b64046d57ce7376ac4814a807dfa56c833ef658 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= +Date: Mon, 1 Jun 2026 10:26:29 +0200 +Subject: [PATCH] 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. + +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/erlang/otp/commit/a3adf63078438c86527d704e23282b7721d8ca12.patch +--- + .../src/http_server/httpd_request_handler.erl | 22 +++++--- + lib/inets/test/httpd_SUITE.erl | 50 ++++++++++++++++++- + 2 files changed, 63 insertions(+), 9 deletions(-) + +diff --git a/lib/inets/src/http_server/httpd_request_handler.erl b/lib/inets/src/http_server/httpd_request_handler.erl +index 4f71c11..9d28714 100644 +--- a/lib/inets/src/http_server/httpd_request_handler.erl ++++ b/lib/inets/src/http_server/httpd_request_handler.erl +@@ -226,7 +226,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; +@@ -234,7 +233,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 -> +@@ -243,13 +242,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}}; +@@ -259,17 +260,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 +diff --git a/lib/inets/test/httpd_SUITE.erl b/lib/inets/test/httpd_SUITE.erl +index 056e7fc..7862082 100644 +--- a/lib/inets/test/httpd_SUITE.erl ++++ b/lib/inets/test/httpd_SUITE.erl +@@ -127,7 +127,11 @@ groups() -> + reload_config_file, + reload_invalid_config_survives + ]}, +- {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]}, +@@ -894,6 +898,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"}]. +-- +2.45.4 + diff --git a/SPECS/erlang/CVE-2026-70399.patch b/SPECS/erlang/CVE-2026-70399.patch new file mode 100644 index 00000000000..16b5475f5b6 --- /dev/null +++ b/SPECS/erlang/CVE-2026-70399.patch @@ -0,0 +1,113 @@ +From 3a3859b69008ed8567eb3ac0c3b633c41b0a8c50 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= +Date: Mon, 1 Jun 2026 16:29:47 +0200 +Subject: [PATCH] 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. + +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/erlang/otp/commit/d94a94c96eb4cccbd6f7d7da5975f0c035b69612.patch +--- + lib/inets/src/http_lib/http_internal.hrl | 1 + + lib/inets/src/http_server/httpd_manager.erl | 47 +++++---------------- + 2 files changed, 11 insertions(+), 37 deletions(-) + +diff --git a/lib/inets/src/http_lib/http_internal.hrl b/lib/inets/src/http_lib/http_internal.hrl +index fbf08f0..20d8c05 100644 +--- 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,{ +diff --git a/lib/inets/src/http_server/httpd_manager.erl b/lib/inets/src/http_server/httpd_manager.erl +index ddaf5f9..e4b150c 100644 +--- a/lib/inets/src/http_server/httpd_manager.erl ++++ b/lib/inets/src/http_server/httpd_manager.erl +@@ -22,6 +22,7 @@ + -module(httpd_manager). + + -include("httpd.hrl"). ++-include("../http_lib/http_internal.hrl"). + + -behaviour(gen_server). + +@@ -224,8 +225,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) -> +@@ -319,22 +320,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, +@@ -424,28 +419,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). + +-- +2.45.4 + diff --git a/SPECS/erlang/CVE-2026-71380.patch b/SPECS/erlang/CVE-2026-71380.patch new file mode 100644 index 00000000000..ada67221a0e --- /dev/null +++ b/SPECS/erlang/CVE-2026-71380.patch @@ -0,0 +1,169 @@ +From 17610c29e085dd82ea865c4ab0d7b37655647613 Mon Sep 17 00:00:00 2001 +From: AllSpark +Date: Fri, 4 Sep 2026 09:09:10 +0000 +Subject: [PATCH] inets: rename max_body_read_timeout option to request_timeout + +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: AI Backport of https://github.com/erlang/otp/commit/81b453aac5a006bb8d26405f2bc3cf24e9d7733c.patch +--- + lib/inets/src/http_lib/http_internal.hrl | 1 + + lib/inets/src/http_server/httpd.erl | 12 ++++++------ + lib/inets/src/http_server/httpd_conf.erl | 7 +++++++ + lib/inets/src/http_server/httpd_request_handler.erl | 10 +++++----- + lib/inets/test/httpd_basic_SUITE.erl | 6 ++++-- + 5 files changed, 23 insertions(+), 13 deletions(-) + +diff --git a/lib/inets/src/http_lib/http_internal.hrl b/lib/inets/src/http_lib/http_internal.hrl +index fbf08f0..2cda61a 100644 +--- 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_REQUEST_READ_TIMEOUT, 60). %% seconds + + %%% Response headers + -record(http_response_h,{ +diff --git a/lib/inets/src/http_server/httpd.erl b/lib/inets/src/http_server/httpd.erl +index d7596ef..dd3c2b7 100644 +--- a/lib/inets/src/http_server/httpd.erl ++++ b/lib/inets/src/http_server/httpd.erl +@@ -97,7 +97,7 @@ 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_clients, integer()} + | {max_header_size, integer()} +@@ -137,7 +137,7 @@ info(Pid) when is_pid(Pid) -> + 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_clients, integer()} + | {max_header_size, integer()} +@@ -175,7 +175,7 @@ info(Pid) when is_pid(Pid) -> + 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_clients, integer()} + | {max_header_size, integer()} +@@ -229,7 +229,7 @@ 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_clients, integer()} + | {max_header_size, integer()} +@@ -268,7 +268,7 @@ 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_clients, integer()} + | {max_header_size, integer()} +@@ -315,7 +315,7 @@ info(Address, Port, Properties) when is_integer(Port) andalso + 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_clients, integer()} + | {max_header_size, integer()} +diff --git a/lib/inets/src/http_server/httpd_conf.erl b/lib/inets/src/http_server/httpd_conf.erl +index affb224..e666cd4 100644 +--- a/lib/inets/src/http_server/httpd_conf.erl ++++ b/lib/inets/src/http_server/httpd_conf.erl +@@ -175,6 +175,13 @@ validate_config_params([{max_body_size, Value} | Rest]) + when is_integer(Value) andalso (Value > 0) -> + validate_config_params(Rest); + validate_config_params([{max_body_size, Value} | _]) -> ++ ++validate_config_params([{request_timeout, Value} | Rest]) ++ when (is_integer(Value) andalso (Value > 0)); ++ Value =:= infinity -> ++ validate_config_params(Rest); ++validate_config_params([{request_timeout, Value} | _]) -> ++ throw({request_timeout, Value}); + throw({max_body_size, Value}); + + validate_config_params([{max_content_length, Value} | Rest]) +diff --git a/lib/inets/src/http_server/httpd_request_handler.erl b/lib/inets/src/http_server/httpd_request_handler.erl +index 4f71c11..ac67e15 100644 +--- a/lib/inets/src/http_server/httpd_request_handler.erl ++++ b/lib/inets/src/http_server/httpd_request_handler.erl +@@ -168,7 +168,7 @@ continue_init(Manager, ConfigDB, SocketType, Socket, Peername, Sockname, + State = #state{mod = Mod, + manager = Manager, + status = Status, +- timeout = TimeOut, ++ keepalive_timeout = TimeOut, + max_keep_alive_request = NrOfRequest, + mfa = MFA, + chunk = chunk_start(MaxChunk)}, +@@ -246,21 +246,21 @@ handle_info({Proto, Socket, Data}, + {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, ++ {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, ++ {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}; + NewMFA -> + setopts(Socket, SockType, [{active, once}]), +diff --git a/lib/inets/test/httpd_basic_SUITE.erl b/lib/inets/test/httpd_basic_SUITE.erl +index fdd78ec..13fc00b 100644 +--- a/lib/inets/test/httpd_basic_SUITE.erl ++++ b/lib/inets/test/httpd_basic_SUITE.erl +@@ -337,7 +337,7 @@ keep_alive_timeout(suite) -> + []; + keep_alive_timeout(Config) when is_list(Config) -> + HttpdConf = proplists:get_value(httpd_conf, Config), +- {ok, Pid} = inets:start(httpd, [{port, 0}, {keep_alive, true}, {keep_alive_timeout, 2} | HttpdConf]), ++ {ok, Pid} = inets:start(httpd, [{port, 0}, {keep_alive, true}, {request_timeout, 2} | HttpdConf]), + Info = httpd:info(Pid), + Port = proplists:get_value(port, Info), + _Address = proplists:get_value(bind_address, Info), +@@ -391,7 +391,9 @@ slowdose(Config) when is_list(Config) -> + receive + after 6000 -> + {error, closed} = gen_tcp:send(Socket, "Hey") +- end. ++ end, ++ inets_test_lib:close(ip_comm, Socket), ++ ok = inets:stop(httpd, Pid). + + %%------------------------------------------------------------------------- + +-- +2.45.4 + diff --git a/SPECS/erlang/CVE-2026-73270.patch b/SPECS/erlang/CVE-2026-73270.patch new file mode 100644 index 00000000000..5f7e525e1f3 --- /dev/null +++ b/SPECS/erlang/CVE-2026-73270.patch @@ -0,0 +1,122 @@ +From 5aeda9a67355012d77cc275c3c6f21bb34c16f3c Mon Sep 17 00:00:00 2001 +From: Konrad Pietrzak +Date: Fri, 7 Aug 2026 13:12:11 +0200 +Subject: [PATCH] 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. + +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/erlang/otp/commit/bac19eb3dbd96cc49b6d8cabc1c04248bf8c79f6.patch +--- + 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 +- + 4 files changed, 40 insertions(+), 4 deletions(-) + +diff --git a/lib/inets/src/http_server/httpd_request.erl b/lib/inets/src/http_server/httpd_request.erl +index 891206c..3b5d90b 100644 +--- a/lib/inets/src/http_server/httpd_request.erl ++++ b/lib/inets/src/http_server/httpd_request.erl +@@ -340,7 +340,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") -> +diff --git a/lib/inets/src/http_server/httpd_util.erl b/lib/inets/src/http_server/httpd_util.erl +index d6c0626..7e52997 100644 +--- a/lib/inets/src/http_server/httpd_util.erl ++++ b/lib/inets/src/http_server/httpd_util.erl +@@ -54,7 +54,8 @@ + 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"}). +@@ -722,3 +723,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 +diff --git a/lib/inets/src/http_server/mod_alias.erl b/lib/inets/src/http_server/mod_alias.erl +index c76f286..80eb9d1 100644 +--- a/lib/inets/src/http_server/mod_alias.erl ++++ b/lib/inets/src/http_server/mod_alias.erl +@@ -218,7 +218,7 @@ percent_decode_path(InitPath) -> + {error, _, _} -> + InitPath; + Path -> +- Path ++ httpd_util:collapse_slashes(Path) + end + end. + %% +diff --git a/lib/inets/src/http_server/mod_auth.erl b/lib/inets/src/http_server/mod_auth.erl +index 6d9d73d..01483dd 100644 +--- a/lib/inets/src/http_server/mod_auth.erl ++++ b/lib/inets/src/http_server/mod_auth.erl +@@ -470,7 +470,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)-> +-- +2.45.4 + diff --git a/SPECS/erlang/CVE-2026-73276.patch b/SPECS/erlang/CVE-2026-73276.patch new file mode 100644 index 00000000000..25181495071 --- /dev/null +++ b/SPECS/erlang/CVE-2026-73276.patch @@ -0,0 +1,128 @@ +From 1f573247a754843a1c7e45a3edfd970479f40d7b Mon Sep 17 00:00:00 2001 +From: Konrad Pietrzak +Date: Mon, 27 Jul 2026 15:21:56 +0200 +Subject: [PATCH] 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}. + +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/erlang/otp/commit/60add5a19e2154560fdff3fe92dba14a68bbd683.patch +--- + lib/inets/src/http_lib/http_request.erl | 11 +++++------ + lib/inets/src/http_server/httpd_request.erl | 12 +++++++++--- + lib/inets/src/http_server/httpd_request_handler.erl | 6 ++++++ + lib/inets/test/httpd_SUITE.erl | 4 +++- + 4 files changed, 23 insertions(+), 10 deletions(-) + +diff --git a/lib/inets/src/http_lib/http_request.erl b/lib/inets/src/http_lib/http_request.erl +index dad441c..ad28ca0 100644 +--- a/lib/inets/src/http_lib/http_request.erl ++++ b/lib/inets/src/http_lib/http_request.erl +@@ -28,18 +28,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{} +diff --git a/lib/inets/src/http_server/httpd_request.erl b/lib/inets/src/http_server/httpd_request.erl +index f687354..0693901 100644 +--- a/lib/inets/src/http_server/httpd_request.erl ++++ b/lib/inets/src/http_server/httpd_request.erl +@@ -203,12 +203,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 -> +@@ -262,12 +265,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)), +diff --git a/lib/inets/src/http_server/httpd_request_handler.erl b/lib/inets/src/http_server/httpd_request_handler.erl +index 81ce00d..ffb8974 100644 +--- a/lib/inets/src/http_server/httpd_request_handler.erl ++++ b/lib/inets/src/http_server/httpd_request_handler.erl +@@ -261,6 +261,12 @@ handle_info({Proto, Socket, Data}, + {stop, normal, State#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}}; ++ + {http_chunk = Module, Function, Args} when ChunkState =/= undefined -> + NewState = handle_chunk(Module, Function, Args, State), + {noreply, NewState}; +diff --git a/lib/inets/test/httpd_SUITE.erl b/lib/inets/test/httpd_SUITE.erl +index 38a5fd1..504427d 100644 +--- a/lib/inets/test/httpd_SUITE.erl ++++ b/lib/inets/test/httpd_SUITE.erl +@@ -1636,7 +1636,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}]). + + %%------------------------------------------------------------------------- +-- +2.45.4 + diff --git a/SPECS/erlang/CVE-2026-73812.patch b/SPECS/erlang/CVE-2026-73812.patch new file mode 100644 index 00000000000..c25d88a348c --- /dev/null +++ b/SPECS/erlang/CVE-2026-73812.patch @@ -0,0 +1,206 @@ +From d295625741d92c816625ace15b5d2ae0e10d7e5b Mon Sep 17 00:00:00 2001 +From: AllSpark +Date: Fri, 4 Sep 2026 09:09:56 +0000 +Subject: [PATCH] inets: Reject requests with both Transfer-Encoding and + Content-Length + +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: AI Backport of https://github.com/erlang/otp/commit/591dc00dc99dc2a426167a3b5257c0c94bd45e91.patch +--- + lib/inets/src/http_server/httpd_internal.hrl | 2 + + lib/inets/src/http_server/httpd_request.erl | 20 ++++++++-- + lib/inets/test/http_test_lib.erl | 42 ++++++++++++++++++-- + lib/inets/test/httpc_SUITE.erl | 2 - + lib/inets/test/httpd_SUITE.erl | 42 ++++++++++++++++++++ + 5 files changed, 100 insertions(+), 8 deletions(-) + +diff --git a/lib/inets/src/http_server/httpd_internal.hrl b/lib/inets/src/http_server/httpd_internal.hrl +index a60fc02..e76f5ec 100644 +--- a/lib/inets/src/http_server/httpd_internal.hrl ++++ b/lib/inets/src/http_server/httpd_internal.hrl +@@ -40,5 +40,7 @@ + {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). +diff --git a/lib/inets/src/http_server/httpd_request.erl b/lib/inets/src/http_server/httpd_request.erl +index 891206c..ed806ac 100644 +--- a/lib/inets/src/http_server/httpd_request.erl ++++ b/lib/inets/src/http_server/httpd_request.erl +@@ -430,11 +430,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(_, _, _) -> +diff --git a/lib/inets/test/http_test_lib.erl b/lib/inets/test/http_test_lib.erl +index 5f58dbb..d653eda 100644 +--- a/lib/inets/test/http_test_lib.erl ++++ b/lib/inets/test/http_test_lib.erl +@@ -148,7 +148,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 -> +@@ -162,7 +163,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 +@@ -184,12 +185,47 @@ 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()}, +diff --git a/lib/inets/test/httpc_SUITE.erl b/lib/inets/test/httpc_SUITE.erl +index a8b197b..2e5d980 100644 +--- a/lib/inets/test/httpc_SUITE.erl ++++ b/lib/inets/test/httpc_SUITE.erl +@@ -1361,12 +1361,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", +diff --git a/lib/inets/test/httpd_SUITE.erl b/lib/inets/test/httpd_SUITE.erl +index 056e7fc..8bc06a4 100644 +--- a/lib/inets/test/httpd_SUITE.erl ++++ b/lib/inets/test/httpd_SUITE.erl +@@ -917,6 +917,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"}]. + +-- +2.45.4 + diff --git a/SPECS/erlang/CVE-2026-74835.patch b/SPECS/erlang/CVE-2026-74835.patch new file mode 100644 index 00000000000..a1589731a00 --- /dev/null +++ b/SPECS/erlang/CVE-2026-74835.patch @@ -0,0 +1,277 @@ +From fbe4c57a9276aaa10852c0c4eefdacd09b1145a8 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= +Date: Tue, 2 Jun 2026 10:41:36 +0200 +Subject: [PATCH] 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. + +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/erlang/otp/commit/0bceff0c2987cae83d9d4a77c5ecacd6d01b8b86.patch +--- + 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(-) + +diff --git a/lib/inets/src/http_lib/http_chunk.erl b/lib/inets/src/http_lib/http_chunk.erl +index 0588937..45de622 100644 +--- a/lib/inets/src/http_lib/http_chunk.erl ++++ b/lib/inets/src/http_lib/http_chunk.erl +@@ -57,8 +57,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 +@@ -134,6 +133,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(<>, +@@ -144,8 +144,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, +@@ -159,6 +158,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 +@@ -168,7 +169,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 +@@ -184,7 +185,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, +@@ -200,9 +201,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 + <> -> +@@ -230,24 +230,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]}; +@@ -296,3 +294,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}}). +diff --git a/lib/inets/src/http_server/httpd_request_handler.erl b/lib/inets/src/http_server/httpd_request_handler.erl +index 9d28714..81ce00d 100644 +--- a/lib/inets/src/http_server/httpd_request_handler.erl ++++ b/lib/inets/src/http_server/httpd_request_handler.erl +@@ -243,6 +243,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}), +@@ -273,9 +274,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 +@@ -510,12 +509,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, +@@ -556,6 +556,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) -> +diff --git a/lib/inets/test/http_format_SUITE.erl b/lib/inets/test/http_format_SUITE.erl +index f822778..6cb2af2 100644 +--- 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)). + +diff --git a/lib/inets/test/httpd_basic_SUITE.erl b/lib/inets/test/httpd_basic_SUITE.erl +index fdd78ec..20fd0b3 100644 +--- 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 + %%------------------------------------------------------------------------- +-- +2.45.4 + diff --git a/SPECS/erlang/CVE-2026-74994.patch b/SPECS/erlang/CVE-2026-74994.patch new file mode 100644 index 00000000000..db8251589fe --- /dev/null +++ b/SPECS/erlang/CVE-2026-74994.patch @@ -0,0 +1,169 @@ +From 705424f040fa133c77444df0366bb27fe23e6f25 Mon Sep 17 00:00:00 2001 +From: Konrad Pietrzak +Date: Fri, 24 Jul 2026 12:42:33 +0200 +Subject: [PATCH] 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. + +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/erlang/otp/commit/6982e381137ede21a4e1faf5fa2dd82321691176.patch +--- + 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(-) + +diff --git a/lib/inets/src/http_server/mod_auth.erl b/lib/inets/src/http_server/mod_auth.erl +index 01483dd..738ac9c 100644 +--- a/lib/inets/src/http_server/mod_auth.erl ++++ b/lib/inets/src/http_server/mod_auth.erl +@@ -420,8 +420,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 -> +@@ -455,9 +456,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. +diff --git a/lib/inets/src/http_server/mod_auth_server.erl b/lib/inets/src/http_server/mod_auth_server.erl +index 1156738..d4a1b38 100644 +--- a/lib/inets/src/http_server/mod_auth_server.erl ++++ b/lib/inets/src/http_server/mod_auth_server.erl +@@ -258,7 +258,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; +diff --git a/lib/inets/test/httpd_SUITE.erl b/lib/inets/test/httpd_SUITE.erl +index 7862082..fa8837a 100644 +--- a/lib/inets/test/httpd_SUITE.erl ++++ b/lib/inets/test/httpd_SUITE.erl +@@ -134,8 +134,8 @@ groups() -> + 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]}, +- {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]}, +@@ -802,6 +802,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"}]. +-- +2.45.4 + diff --git a/SPECS/erlang/CVE-2026-75538.patch b/SPECS/erlang/CVE-2026-75538.patch new file mode 100644 index 00000000000..b136acb070c --- /dev/null +++ b/SPECS/erlang/CVE-2026-75538.patch @@ -0,0 +1,60 @@ +From 0a5ee1e2f2ba34c48da158a37a9dfbfc10c22336 Mon Sep 17 00:00:00 2001 +From: Raimo Niskanen +Date: Fri, 21 Aug 2026 17:22:41 +0200 +Subject: [PATCH] Avoid signed int overflows + +Signed-off-by: Azure Linux Security Servicing Account +Upstream-reference: https://github.com/erlang/otp/commit/08e8efdba8500d2d6f54c6b1de1492b228017c9b.patch +--- + erts/emulator/beam/packet_parser.c | 16 ++++++++-------- + erts/emulator/drivers/common/inet_drv.c | 7 ++++++- + 2 files changed, 14 insertions(+), 9 deletions(-) + +diff --git a/erts/emulator/beam/packet_parser.c b/erts/emulator/beam/packet_parser.c +index a349c3f..8e3430b 100644 +--- 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; +diff --git a/erts/emulator/drivers/common/inet_drv.c b/erts/emulator/drivers/common/inet_drv.c +index c7c3b73..d5e5567 100644 +--- a/erts/emulator/drivers/common/inet_drv.c ++++ b/erts/emulator/drivers/common/inet_drv.c +@@ -11425,7 +11425,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; +-- +2.45.4 + diff --git a/SPECS/erlang/erlang.spec b/SPECS/erlang/erlang.spec index cc4877fe12f..195069887e2 100644 --- a/SPECS/erlang/erlang.spec +++ b/SPECS/erlang/erlang.spec @@ -2,7 +2,7 @@ Summary: erlang Name: erlang Version: 26.2.5.21 -Release: 5%{?dist} +Release: 6%{?dist} License: Apache-2.0 Vendor: Microsoft Corporation Distribution: Azure Linux @@ -25,6 +25,17 @@ Patch12: CVE-2026-53422.patch Patch13: CVE-2026-54887.patch Patch14: CVE-2026-55950.patch Patch15: CVE-2026-59250.patch +Patch16: CVE-2026-55951.patch +Patch17: CVE-2026-66357.patch +Patch18: CVE-2026-69664.patch +Patch19: CVE-2026-70399.patch +Patch20: CVE-2026-71380.patch +Patch21: CVE-2026-73270.patch +Patch22: CVE-2026-73276.patch +Patch23: CVE-2026-73812.patch +Patch24: CVE-2026-74835.patch +Patch25: CVE-2026-74994.patch +Patch26: CVE-2026-75538.patch BuildRequires: ncurses-devel BuildRequires: openssl-devel BuildRequires: unixODBC-devel @@ -69,6 +80,9 @@ export ERL_TOP=`pwd` %{_libdir}/erlang/* %changelog +* Fri Sep 04 2026 Azure Linux Security Servicing Account - 26.2.5.21-6 +- Patch for CVE-2026-75538, CVE-2026-74994, CVE-2026-74835, CVE-2026-73812, CVE-2026-73276, CVE-2026-73270, CVE-2026-71380, CVE-2026-70399, CVE-2026-69664, CVE-2026-66357, CVE-2026-55951 + * Tue Aug 11 2026 Aditya Singh - 26.2.5.21-5 - Patch for CVE-2026-53422, CVE-2026-54887, CVE-2026-55950, CVE-2026-59250 From b4fb09da9385c79e449ebbc3ea3cbd7eb1605651 Mon Sep 17 00:00:00 2001 From: Kshitiz Godara Date: Sun, 6 Sep 2026 07:01:27 +0000 Subject: [PATCH 2/3] Modifying the patch files Modify patch files to apply for the azure linux version and add no patch files for cves. --- SPECS/erlang/CVE-2026-55951.patch | 1 + SPECS/erlang/CVE-2026-59696.nopatch | 1 + SPECS/erlang/CVE-2026-66835.nopatch | 1 + SPECS/erlang/CVE-2026-70405.nopatch | 1 + SPECS/erlang/CVE-2026-70409.nopatch | 1 + SPECS/erlang/CVE-2026-71380.patch | 322 ++++++++++++++++++++++------ SPECS/erlang/CVE-2026-71562.nopatch | 1 + SPECS/erlang/CVE-2026-73270.patch | 9 +- SPECS/erlang/CVE-2026-73276.patch | 20 +- SPECS/erlang/CVE-2026-74835.patch | 2 +- SPECS/erlang/erlang.spec | 16 +- 11 files changed, 275 insertions(+), 100 deletions(-) create mode 100644 SPECS/erlang/CVE-2026-59696.nopatch create mode 100644 SPECS/erlang/CVE-2026-66835.nopatch create mode 100644 SPECS/erlang/CVE-2026-70405.nopatch create mode 100644 SPECS/erlang/CVE-2026-70409.nopatch create mode 100644 SPECS/erlang/CVE-2026-71562.nopatch diff --git a/SPECS/erlang/CVE-2026-55951.patch b/SPECS/erlang/CVE-2026-55951.patch index 3a5826ee60f..f562ca94202 100644 --- a/SPECS/erlang/CVE-2026-55951.patch +++ b/SPECS/erlang/CVE-2026-55951.patch @@ -5,6 +5,7 @@ Subject: [PATCH] inets,stdlib,eldap: Bound integer parsing to mitigate DoS Signed-off-by: Azure Linux Security Servicing Account Upstream-reference: AI Backport of https://github.com/erlang/otp/commit/aba0fe8c2d700bf4ac94607cf7f00e53bbe4042d.patch +Also-fixes: CVE-2026-59696, CVE-2026-70405, CVE-2026-70409, CVE-2026-71562 --- lib/eldap/src/eldap.erl | 6 +- lib/inets/src/http_client/httpc.erl | 52 ++++-- diff --git a/SPECS/erlang/CVE-2026-59696.nopatch b/SPECS/erlang/CVE-2026-59696.nopatch new file mode 100644 index 00000000000..4a8efddf52e --- /dev/null +++ b/SPECS/erlang/CVE-2026-59696.nopatch @@ -0,0 +1 @@ +# Fixed by CVE-2026-55951.patch (same upstream commit aba0fe8c2d700bf4ac94607cf7f00e53bbe4042d) diff --git a/SPECS/erlang/CVE-2026-66835.nopatch b/SPECS/erlang/CVE-2026-66835.nopatch new file mode 100644 index 00000000000..bf560f93568 --- /dev/null +++ b/SPECS/erlang/CVE-2026-66835.nopatch @@ -0,0 +1 @@ +# Fixed by CVE-2026-73270.patch (same upstream commit bac19eb3dbd96cc49b6d8cabc1c04248bf8c79f6) diff --git a/SPECS/erlang/CVE-2026-70405.nopatch b/SPECS/erlang/CVE-2026-70405.nopatch new file mode 100644 index 00000000000..4a8efddf52e --- /dev/null +++ b/SPECS/erlang/CVE-2026-70405.nopatch @@ -0,0 +1 @@ +# Fixed by CVE-2026-55951.patch (same upstream commit aba0fe8c2d700bf4ac94607cf7f00e53bbe4042d) diff --git a/SPECS/erlang/CVE-2026-70409.nopatch b/SPECS/erlang/CVE-2026-70409.nopatch new file mode 100644 index 00000000000..4a8efddf52e --- /dev/null +++ b/SPECS/erlang/CVE-2026-70409.nopatch @@ -0,0 +1 @@ +# Fixed by CVE-2026-55951.patch (same upstream commit aba0fe8c2d700bf4ac94607cf7f00e53bbe4042d) diff --git a/SPECS/erlang/CVE-2026-71380.patch b/SPECS/erlang/CVE-2026-71380.patch index ada67221a0e..1f6cb4681ee 100644 --- a/SPECS/erlang/CVE-2026-71380.patch +++ b/SPECS/erlang/CVE-2026-71380.patch @@ -1,169 +1,355 @@ From 17610c29e085dd82ea865c4ab0d7b37655647613 Mon Sep 17 00:00:00 2001 -From: AllSpark -Date: Fri, 4 Sep 2026 09:09:10 +0000 -Subject: [PATCH] inets: rename max_body_read_timeout option to request_timeout +From: Konrad Pietrzak +Date: Wed, 19 Aug 2026 17:49:02 +0200 +Subject: [PATCH] inets: Add httpd request_timeout option + + Backport of the max_body_read_timeout option together with the follow-up + rename to request_timeout, squashed into a single change because OTP + 26.2.5.21 never carried the intermediate option name. + + - Add a request_timeout httpd option (default 60 seconds) that bounds the + idle time between message body reads, closing slow-read connections + with 408 instead of letting them hold a handler indefinitely. + - Split the single #state.timeout/timer pair into keepalive_timeout and + request_timeout, and tag the timer reference so the two timers cannot + overwrite each other. + - Rename activate_request_timeout/cancel_request_timeout to + activate_keepalive_timeout/cancel_timeout and add the request timer + counterparts. + - Widen keep_alive_timeout and request_timeout to accept infinity. Signed-off-by: Azure Linux Security Servicing Account -Upstream-reference: AI Backport of https://github.com/erlang/otp/commit/81b453aac5a006bb8d26405f2bc3cf24e9d7733c.patch +Upstream-reference: https://github.com/erlang/otp/commit/f46293c977608e5656aaa2a78a5492faccd0fe09.patch +Upstream-reference: https://github.com/erlang/otp/commit/81b453aac5a006bb8d26405f2bc3cf24e9d7733c.patch --- lib/inets/src/http_lib/http_internal.hrl | 1 + - lib/inets/src/http_server/httpd.erl | 12 ++++++------ - lib/inets/src/http_server/httpd_conf.erl | 7 +++++++ - lib/inets/src/http_server/httpd_request_handler.erl | 10 +++++----- - lib/inets/test/httpd_basic_SUITE.erl | 6 ++++-- - 5 files changed, 23 insertions(+), 13 deletions(-) + lib/inets/src/http_server/httpd.erl | 18 ++++++++++++------ + lib/inets/src/http_server/httpd_conf.erl | 6 ++++++ + lib/inets/src/http_server/httpd_request_handler.erl | 111 ++++++++++++++++++++-------------------- + 4 files changed, 87 insertions(+), 49 deletions(-) diff --git a/lib/inets/src/http_lib/http_internal.hrl b/lib/inets/src/http_lib/http_internal.hrl -index fbf08f0..2cda61a 100644 --- 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). +@@ -31,6 +31,7 @@ -define(HTTP_MAX_METHOD_STRING, 20). -define(HTTP_MAX_CONTENT_LENGTH, 100000000). + -define(HTTP_MAX_CLIENTS, 150). +-define(HTTP_REQUEST_READ_TIMEOUT, 60). %% seconds %%% Response headers -record(http_response_h,{ diff --git a/lib/inets/src/http_server/httpd.erl b/lib/inets/src/http_server/httpd.erl -index d7596ef..dd3c2b7 100644 --- a/lib/inets/src/http_server/httpd.erl +++ b/lib/inets/src/http_server/httpd.erl -@@ -97,7 +97,7 @@ reload_config(ConfigFile, Mode) -> +@@ -97,8 +97,9 @@ 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()} ++ | {request_timeout, pos_integer() | infinity} | {max_clients, integer()} | {max_header_size, integer()} -@@ -137,7 +137,7 @@ info(Pid) when is_pid(Pid) -> + | {max_content_length, integer()} +@@ -137,8 +138,9 @@ 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()} ++ | {request_timeout, pos_integer() | infinity} | {max_clients, integer()} | {max_header_size, integer()} -@@ -175,7 +175,7 @@ info(Pid) when is_pid(Pid) -> + | {max_content_length, integer()} +@@ -175,8 +177,9 @@ 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()} ++ | {request_timeout, pos_integer() | infinity} | {max_clients, integer()} | {max_header_size, integer()} -@@ -229,7 +229,7 @@ info(Address, Port) when is_integer(Port) -> + | {max_content_length, integer()} +@@ -229,8 +232,9 @@ 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()} ++ | {request_timeout, pos_integer() | infinity} | {max_clients, integer()} | {max_header_size, integer()} -@@ -268,7 +268,7 @@ info(Address, Port) when is_integer(Port) -> + | {max_content_length, integer()} +@@ -268,8 +272,9 @@ 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()} ++ | {request_timeout, pos_integer() | infinity} | {max_clients, integer()} | {max_header_size, integer()} -@@ -315,7 +315,7 @@ info(Address, Port, Properties) when is_integer(Port) andalso + | {max_content_length, integer()} +@@ -315,8 +320,9 @@ 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()} ++ | {request_timeout, pos_integer() | infinity} | {max_clients, integer()} | {max_header_size, integer()} + | {max_content_length, integer()} diff --git a/lib/inets/src/http_server/httpd_conf.erl b/lib/inets/src/http_server/httpd_conf.erl -index affb224..e666cd4 100644 --- a/lib/inets/src/http_server/httpd_conf.erl +++ b/lib/inets/src/http_server/httpd_conf.erl -@@ -175,6 +175,13 @@ validate_config_params([{max_body_size, Value} | Rest]) - when is_integer(Value) andalso (Value > 0) -> - validate_config_params(Rest); +@@ -177,6 +177,12 @@ validate_config_params([{max_body_size, Value} | _]) -> -+ + throw({max_body_size, Value}); + +validate_config_params([{request_timeout, Value} | Rest]) -+ when (is_integer(Value) andalso (Value > 0)); -+ Value =:= infinity -> ++ when (is_integer(Value) andalso (Value > 0)) orelse (Value =:= infinity) -> + validate_config_params(Rest); +validate_config_params([{request_timeout, Value} | _]) -> + throw({request_timeout, Value}); - throw({max_body_size, Value}); - ++ validate_config_params([{max_content_length, Value} | Rest]) + when is_integer(Value) andalso (Value > 0) -> + validate_config_params(Rest); diff --git a/lib/inets/src/http_server/httpd_request_handler.erl b/lib/inets/src/http_server/httpd_request_handler.erl -index 4f71c11..ac67e15 100644 --- a/lib/inets/src/http_server/httpd_request_handler.erl +++ b/lib/inets/src/http_server/httpd_request_handler.erl -@@ -168,7 +168,7 @@ continue_init(Manager, ConfigDB, SocketType, Socket, Peername, Sockname, +@@ -44,8 +44,10 @@ + mfa, %% {Module, Function, Args} + max_keep_alive_request = infinity, %% integer() | infinity + response_sent = false :: boolean(), +- timeout, %% infinity | integer() > 0 +- timer :: 'undefined' | reference(), % Request timer ++ keepalive_timeout, %% infinity | integer() > 0, milliseconds ++ request_timeout, %% infinity | integer() > 0, milliseconds ++ timer :: 'undefined' | ++ {reference(), keep_alive_timeout | request_timeout}, + headers = #http_request_h{}, + body, %% binary() + data, %% The total data received in bits, checked after 10s +@@ -104,7 +106,7 @@ + Sockname = http_transport:sockname(SocketType, Socket), + + %%Timeout value is in seconds we want it in milliseconds +- KeepAliveTimeOut = 1000 * httpd_util:lookup(ConfigDB, keep_alive_timeout, 150), ++ KeepAliveTimeOut = get_keepalive_timeout(ConfigDB), + + case http_transport:negotiate(SocketType, Socket, ?HANDSHAKE_TIMEOUT) of + {error, {tls_alert, {_, AlertDesc}} = Error} -> +@@ -142,6 +144,7 @@ + MaxURISize = max_uri_size(ConfigDB), + NrOfRequest = max_keep_alive_request(ConfigDB), + MaxContentLen = max_content_length(ConfigDB), ++ RequestTimeout = get_request_timeout(ConfigDB), + Customize = customize(ConfigDB), + MaxChunk = max_client_body_chunk(ConfigDB), + +@@ -168,13 +171,14 @@ State = #state{mod = Mod, manager = Manager, status = Status, - timeout = TimeOut, + keepalive_timeout = TimeOut, ++ request_timeout = RequestTimeout, max_keep_alive_request = NrOfRequest, mfa = MFA, chunk = chunk_start(MaxChunk)}, -@@ -246,21 +246,21 @@ handle_info({Proto, Socket, Data}, + 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. +@@ -226,55 +230,41 @@ + (Proto =:= ssl) orelse + (Proto =:= dummy)) andalso is_binary(Data)) -> + +- NewDataSize = case State#state.byte_limit of +- undefined -> +- undefined; +- _ -> +- State#state.data + byte_size(Data) +- end, ++ RequestTimeoutState = activate_request_timeout(cancel_timeout(State)), + + try Module:Function([Data | Args]) of + {ok, Result} -> +- NewState = case NewDataSize of +- undefined -> +- cancel_request_timeout(State); +- _ -> +- set_new_data_size(cancel_request_timeout(State), NewDataSize) +- end, ++ 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}), +- 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}}; +- mod = NewModData}}; ++ httpd_response:send_status(NewModData, ErrCode, ErrStr, {max_size, MaxSize}), ++ {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), + NewModData = ModData#mod{http_version = Version}, +- httpd_response:send_status(NewModData, ErrCode, ErrStr), - {stop, normal, State#state{response_sent = true, -+ {stop, normal, RequestTimeoutState#state{response_sent = true, - mod = NewModData}}; +- mod = NewModData}}; ++ httpd_response:send_status(NewModData, ErrCode, ErrStr), ++ {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}; - NewMFA -> - setopts(Socket, SockType, [{active, once}]), -diff --git a/lib/inets/test/httpd_basic_SUITE.erl b/lib/inets/test/httpd_basic_SUITE.erl -index fdd78ec..13fc00b 100644 ---- a/lib/inets/test/httpd_basic_SUITE.erl -+++ b/lib/inets/test/httpd_basic_SUITE.erl -@@ -337,7 +337,7 @@ keep_alive_timeout(suite) -> - []; - keep_alive_timeout(Config) when is_list(Config) -> - HttpdConf = proplists:get_value(httpd_conf, Config), -- {ok, Pid} = inets:start(httpd, [{port, 0}, {keep_alive, true}, {keep_alive_timeout, 2} | HttpdConf]), -+ {ok, Pid} = inets:start(httpd, [{port, 0}, {keep_alive, true}, {request_timeout, 2} | HttpdConf]), - Info = httpd:info(Pid), - Port = proplists:get_value(port, Info), - _Address = proplists:get_value(bind_address, Info), -@@ -391,7 +391,9 @@ slowdose(Config) when is_list(Config) -> ++ 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 -> ++ {_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 ++ 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) ++ handle_chunk_size_error(Error, RequestTimeoutState) + end; + + %% Error cases +@@ -345,7 +335,7 @@ + do_terminate(State). + + do_terminate(#state{mod = ModData} = State) -> +- cancel_request_timeout(State), ++ cancel_timeout(State), + httpd_socket:close(ModData#mod.socket_type, ModData#mod.socket). + + format_status(normal, [_, State]) -> +@@ -373,8 +363,11 @@ + %%-------------------------------------------------------------------- + %%% Internal functions + %%-------------------------------------------------------------------- +-set_new_data_size(State, NewData) -> +- State#state{data = NewData}. ++update_data_size(#state{byte_limit = undefined} = State, _Data) -> ++ State; ++update_data_size(State, Data) -> ++ State#state{data = State#state.data + byte_size(Data)}. ++ + await_socket_ownership_transfer(AcceptTimeout) -> receive - after 6000 -> - {error, closed} = gen_tcp:send(Socket, "Hey") -- end. -+ end, -+ inets_test_lib:close(ip_comm, Socket), -+ ok = inets:stop(httpd, Pid). + {socket_ownership_transfered, SocketType, Socket} -> +@@ -656,9 +649,11 @@ + 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, ++ mod = ModData, + chunk = {last, CbState}, + max_keep_alive_request = Max} = State) when Max > 0 -> + {NewBody, Data} = httpd_request:body_data(Headers, Body), +@@ -709,7 +704,7 @@ + chunk = chunk_start(MaxChunk), + response_sent = false}, + +- NewState = activate_request_timeout(TmpState), ++ NewState = activate_keepalive_timeout(TmpState), + + case Data of + <<>> -> +@@ -722,9 +717,26 @@ + handle_next_request(State, _) -> + {stop, normal, State}. + +-activate_request_timeout(#state{timeout = Time} = 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}. ++ 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(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. ++ + data_receive_counter(State, Byte_limit) -> + case Byte_limit of + false -> +@@ -733,9 +745,9 @@ + erlang:send_after(3000, self(), check_data_first), + State#state{data = 0, byte_limit = Nr} + end. +-cancel_request_timeout(#state{timer = undefined} = State) -> ++cancel_timeout(#state{timer = undefined} = State) -> + State; +-cancel_request_timeout(#state{timer = Timer} = State) -> ++cancel_timeout(#state{timer = {Timer, _}} = State) -> + erlang:cancel_timer(Timer), + receive + timeout -> +@@ -772,6 +784,19 @@ + max_content_length(ConfigDB) -> + httpd_util:lookup(ConfigDB, max_content_length, ?HTTP_MAX_CONTENT_LENGTH). + ++%% Timeouts are configured in seconds, the state keeps milliseconds ++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). -- 2.45.4 - diff --git a/SPECS/erlang/CVE-2026-71562.nopatch b/SPECS/erlang/CVE-2026-71562.nopatch new file mode 100644 index 00000000000..4a8efddf52e --- /dev/null +++ b/SPECS/erlang/CVE-2026-71562.nopatch @@ -0,0 +1 @@ +# Fixed by CVE-2026-55951.patch (same upstream commit aba0fe8c2d700bf4ac94607cf7f00e53bbe4042d) diff --git a/SPECS/erlang/CVE-2026-73270.patch b/SPECS/erlang/CVE-2026-73270.patch index 5f7e525e1f3..264f31c4165 100644 --- a/SPECS/erlang/CVE-2026-73270.patch +++ b/SPECS/erlang/CVE-2026-73270.patch @@ -17,12 +17,13 @@ Subject: [PATCH] inets: Canonicalize request path before mod_auth directory Signed-off-by: Azure Linux Security Servicing Account Upstream-reference: https://github.com/erlang/otp/commit/bac19eb3dbd96cc49b6d8cabc1c04248bf8c79f6.patch +Also-fixes: CVE-2026-66835 --- lib/inets/src/http_server/httpd_request.erl | 17 ++++++++++++++- - lib/inets/src/http_server/httpd_util.erl | 23 ++++++++++++++++++++- + lib/inets/src/http_server/httpd_util.erl | 22 ++++++++++++++++++++- lib/inets/src/http_server/mod_alias.erl | 2 +- lib/inets/src/http_server/mod_auth.erl | 2 +- - 4 files changed, 40 insertions(+), 4 deletions(-) + 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/lib/inets/src/http_server/httpd_request.erl b/lib/inets/src/http_server/httpd_request.erl index 891206c..3b5d90b 100644 @@ -66,7 +67,7 @@ index d6c0626..7e52997 100644 -removed({flatlength, 1, "use erlang:iolist_size/1 instead"}). -removed({hexlist_to_integer, 1, "use erlang:list_to_integer/2 with base 16 instead"}). -@@ -722,3 +723,23 @@ mod_error_logging(Mod, ConfigDB, Report) -> +@@ -722,3 +723,22 @@ mod_error_logging(Mod, ConfigDB, Report) -> _ -> ok end. @@ -80,7 +81,6 @@ index d6c0626..7e52997 100644 +%% 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, []). + @@ -90,7 +90,6 @@ index d6c0626..7e52997 100644 + collapse_slashes([$/ | Rest], Acc); +collapse_slashes([C | Rest], Acc) -> + collapse_slashes(Rest, [C | Acc]). -\ No newline at end of file diff --git a/lib/inets/src/http_server/mod_alias.erl b/lib/inets/src/http_server/mod_alias.erl index c76f286..80eb9d1 100644 --- a/lib/inets/src/http_server/mod_alias.erl diff --git a/SPECS/erlang/CVE-2026-73276.patch b/SPECS/erlang/CVE-2026-73276.patch index 25181495071..d958e37ea7a 100644 --- a/SPECS/erlang/CVE-2026-73276.patch +++ b/SPECS/erlang/CVE-2026-73276.patch @@ -20,9 +20,8 @@ Upstream-reference: https://github.com/erlang/otp/commit/60add5a19e2154560fdff3f --- lib/inets/src/http_lib/http_request.erl | 11 +++++------ lib/inets/src/http_server/httpd_request.erl | 12 +++++++++--- - lib/inets/src/http_server/httpd_request_handler.erl | 6 ++++++ lib/inets/test/httpd_SUITE.erl | 4 +++- - 4 files changed, 23 insertions(+), 10 deletions(-) + 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/inets/src/http_lib/http_request.erl b/lib/inets/src/http_lib/http_request.erl index dad441c..ad28ca0 100644 @@ -91,23 +90,6 @@ index f687354..0693901 100644 Current, Max, Options, Result); {error, Reason} -> HttpVersion = lists:nth(3, lists:reverse(Result)), -diff --git a/lib/inets/src/http_server/httpd_request_handler.erl b/lib/inets/src/http_server/httpd_request_handler.erl -index 81ce00d..ffb8974 100644 ---- a/lib/inets/src/http_server/httpd_request_handler.erl -+++ b/lib/inets/src/http_server/httpd_request_handler.erl -@@ -261,6 +261,12 @@ handle_info({Proto, Socket, Data}, - {stop, normal, State#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}}; -+ - {http_chunk = Module, Function, Args} when ChunkState =/= undefined -> - NewState = handle_chunk(Module, Function, Args, State), - {noreply, NewState}; diff --git a/lib/inets/test/httpd_SUITE.erl b/lib/inets/test/httpd_SUITE.erl index 38a5fd1..504427d 100644 --- a/lib/inets/test/httpd_SUITE.erl diff --git a/SPECS/erlang/CVE-2026-74835.patch b/SPECS/erlang/CVE-2026-74835.patch index a1589731a00..02b26d0d56e 100644 --- a/SPECS/erlang/CVE-2026-74835.patch +++ b/SPECS/erlang/CVE-2026-74835.patch @@ -253,7 +253,7 @@ index fdd78ec..20fd0b3 100644 + "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 --git a/SPECS/erlang/erlang.spec b/SPECS/erlang/erlang.spec index 195069887e2..4175cf4117c 100644 --- a/SPECS/erlang/erlang.spec +++ b/SPECS/erlang/erlang.spec @@ -29,13 +29,14 @@ Patch16: CVE-2026-55951.patch Patch17: CVE-2026-66357.patch Patch18: CVE-2026-69664.patch Patch19: CVE-2026-70399.patch -Patch20: CVE-2026-71380.patch -Patch21: CVE-2026-73270.patch -Patch22: CVE-2026-73276.patch -Patch23: CVE-2026-73812.patch -Patch24: CVE-2026-74835.patch -Patch25: CVE-2026-74994.patch -Patch26: CVE-2026-75538.patch +Patch20: CVE-2026-73270.patch +Patch21: CVE-2026-73276.patch +Patch22: CVE-2026-73812.patch +Patch23: CVE-2026-74835.patch +Patch24: CVE-2026-74994.patch +Patch25: CVE-2026-75538.patch +# Applied last: rebased on top of the other inets fixes it shares context with. +Patch26: CVE-2026-71380.patch BuildRequires: ncurses-devel BuildRequires: openssl-devel BuildRequires: unixODBC-devel @@ -82,6 +83,7 @@ export ERL_TOP=`pwd` %changelog * Fri Sep 04 2026 Azure Linux Security Servicing Account - 26.2.5.21-6 - Patch for CVE-2026-75538, CVE-2026-74994, CVE-2026-74835, CVE-2026-73812, CVE-2026-73276, CVE-2026-73270, CVE-2026-71380, CVE-2026-70399, CVE-2026-69664, CVE-2026-66357, CVE-2026-55951 +- Also covers CVE-2026-71562, CVE-2026-70409, CVE-2026-70405, CVE-2026-66835, CVE-2026-59696, which share upstream fixes with the patches above * Tue Aug 11 2026 Aditya Singh - 26.2.5.21-5 From 2706d29dd88954356b99cd1160b9c9918dd1d0ea Mon Sep 17 00:00:00 2001 From: Kshitiz Godara Date: Mon, 7 Sep 2026 04:38:27 +0000 Subject: [PATCH 3/3] Modifying patch for dialyzer --- SPECS/erlang/CVE-2026-71380.patch | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/SPECS/erlang/CVE-2026-71380.patch b/SPECS/erlang/CVE-2026-71380.patch index 1f6cb4681ee..06daf69ae10 100644 --- a/SPECS/erlang/CVE-2026-71380.patch +++ b/SPECS/erlang/CVE-2026-71380.patch @@ -25,8 +25,8 @@ Upstream-reference: https://github.com/erlang/otp/commit/81b453aac5a006bb8d26405 lib/inets/src/http_lib/http_internal.hrl | 1 + lib/inets/src/http_server/httpd.erl | 18 ++++++++++++------ lib/inets/src/http_server/httpd_conf.erl | 6 ++++++ - lib/inets/src/http_server/httpd_request_handler.erl | 111 ++++++++++++++++++++-------------------- - 4 files changed, 87 insertions(+), 49 deletions(-) + lib/inets/src/http_server/httpd_request_handler.erl | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------- + 4 files changed, 81 insertions(+), 49 deletions(-) diff --git a/lib/inets/src/http_lib/http_internal.hrl b/lib/inets/src/http_lib/http_internal.hrl --- a/lib/inets/src/http_lib/http_internal.hrl @@ -290,36 +290,30 @@ diff --git a/lib/inets/src/http_server/httpd_request_handler.erl b/lib/inets/src case Data of <<>> -> -@@ -722,9 +717,26 @@ +@@ -722,9 +717,20 @@ handle_next_request(State, _) -> {stop, normal, State}. -activate_request_timeout(#state{timeout = Time} = 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) -> ++activate_keepalive_timeout(State0) -> ++ #state{keepalive_timeout = Time} = State = cancel_timeout(State0), Ref = erlang:send_after(Time, self(), timeout), - State#state{timer = Ref}. + 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(cancel_timeout(State)); -+activate_request_timeout(#state{timer = undefined, -+ request_timeout = Time} = State) -> ++activate_request_timeout(State0) -> ++ #state{request_timeout = Time} = State = cancel_timeout(State0), + Ref = erlang:send_after(Time, self(), timeout), -+ State#state{timer = {Ref, request_timeout}}; -+activate_request_timeout(State) -> -+ State. ++ State#state{timer = {Ref, request_timeout}}. + data_receive_counter(State, Byte_limit) -> case Byte_limit of false -> -@@ -733,9 +745,9 @@ +@@ -733,9 +739,9 @@ erlang:send_after(3000, self(), check_data_first), State#state{data = 0, byte_limit = Nr} end. @@ -331,7 +325,7 @@ diff --git a/lib/inets/src/http_server/httpd_request_handler.erl b/lib/inets/src erlang:cancel_timer(Timer), receive timeout -> -@@ -772,6 +784,19 @@ +@@ -772,6 +778,19 @@ max_content_length(ConfigDB) -> httpd_util:lookup(ConfigDB, max_content_length, ?HTTP_MAX_CONTENT_LENGTH).