)]}'
{
  "log": [
    {
      "commit": "0f33a458adbdaeeea8ea040fdf3875cf40f33615",
      "tree": "e18ca9a399701e07d800b196b8684f423ae0b4ef",
      "parents": [
        "c3ae5b6bfbac436596e3aba32ce730d181d32e62",
        "247a8884e1805a1361990734a6b1b5f789fb1c83"
      ],
      "author": {
        "name": "remm",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 17:50:04 2026 +0200"
      },
      "committer": {
        "name": "remm",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 17:50:04 2026 +0200"
      },
      "message": "Merge branch \u0027main\u0027 of git@github.com:apache/tomcat.git into main"
    },
    {
      "commit": "c3ae5b6bfbac436596e3aba32ce730d181d32e62",
      "tree": "528f3d3da43f472dfce78bc84d80bbac214bb61e",
      "parents": [
        "22d114371e8c313c3280822f53c3ee9b7d0185d4"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 14:53:25 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 14:53:25 2026 +0200"
      },
      "message": "Remove stray raw path entry added to the WebSocket template match map when a programmatic registration duplicates an annotated POJO template path\n\nThe template match map is keyed by the normalized path in which named\ntemplate parameters are replaced with numeric indices. In the case in\nwhich a programmatic registration duplicates the template path of an\nannotated POJO for the same endpoint class, the specification requires\nthe new match to be ignored. The code copied from the exact match\nbranch below restored the old match into the map using the raw path as\nthe key. Unlike the exact match branch, the template branch uses\nputIfAbsent() which does not modify the map when a match already\nexists, so no restoration is required and the put instead added a\nsecond, raw path keyed entry to the map for every such registration,\nboth keys referencing the same match.\n\nThe redundant put is removed. The exact match branch is unchanged\nsince there the preceding put() does overwrite the old match and the\nrestoration is required.\n\nThe regression test extends testDuplicatePaths22, which exercises this\ncode path, to assert via reflection on the template match map that\nexactly one entry, keyed by the normalized path, remains after the\nduplicate registration is ignored.\n"
    },
    {
      "commit": "22d114371e8c313c3280822f53c3ee9b7d0185d4",
      "tree": "33ceeac48b5b5e429bccdb92a5cf1f6cb28c3f73",
      "parents": [
        "2b87812ca2ef70a28de0c48ccc8e7e0d79bfc6fa"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 14:24:56 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 14:24:56 2026 +0200"
      },
      "message": "Reject WebSocket upgrade requests with an invalid percent-escape sequence in the query string with a 400 response\n\nInvalid percent-escape sequences in a query string are not rejected by\nthe HTTP parser since the individual characters are legal. Escape\npairs are only checked when the query string is decoded. Building the\nabsolute request URI for the handshake in WsHandshakeRequest uses\njava.net.URI with the raw query string, so a sequence such as \"?x\u003d%ZZ\"\nresulted in a URISyntaxException that was wrapped in an\nIllegalArgumentException which nothing caught. The uncaught runtime\nexception propagated out of the filter chain and produced an HTTP 500\nresponse and a SEVERE container log entry with a stack trace for every\nsuch request from an untrusted client, instead of the 400 Bad Request\nresponse that every other handshake validation failure produces.\n\nUpgradeUtil.doUpgrade() now catches the failure to construct the\nWsHandshakeRequest and rejects the handshake with a 400 response. The\nresponse is reset before the error is sent so the rejection is not\naccompanied by the Upgrade, Connection and Sec-WebSocket-Accept\nheaders that indicate a successful handshake.\n\nAdds TestHandshakeRequest covering invalid escape pairs, incomplete\nescapes and trailing percent characters in the query string (400), as\nwell as valid escapes and an empty query string (101). Note that a\nquery string that is valid URI syntax but undecodable as UTF-8, such\nas \"?x\u003d%C0%80\", is already rejected with a 400 through the separate\nInvalidParameterException handling in the valve layer; that behavior\nis pinned by a test as well.\n"
    },
    {
      "commit": "2b87812ca2ef70a28de0c48ccc8e7e0d79bfc6fa",
      "tree": "c7d7072be42b586de29a0c5527676643c50a9c76",
      "parents": [
        "6e6f2fbd8dfbe5f7243a1fcf9415d3854196b902"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 14:01:43 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 14:01:43 2026 +0200"
      },
      "message": "Fix covariant return type overrides of annotated POJO lifecycle methods wrongly rejected as duplicates\n\nA sub-class method that overrides an annotated life cycle method of a\nsuper class is permitted by the JLS to use a covariant return type.\nisMethodOverride() in PojoMethodMapping compared return types using\nequals() so such overrides were not recognised as overrides. Re-\ndeclaring the annotation on the override therefore triggered a\ndeployment failure (\"Duplicate annotations [...]\") even though an\noverride with an identical return type re-declaring the annotation was\naccepted.\n\nisMethodOverride() now checks that the return types are compatible\nrather than identical. The methods are passed to isMethodOverride() in\ndifferent orders by the various call sites so the check tests\nassignability in both directions. Methods with the same name and\nparameter types within a single class hierarchy are override\nequivalent by definition, since the compiler enforces return type\ncovariance, so this cannot wrongly match methods that are not related\nby overriding.\n\nAdds regression tests covering an annotated covariant override that\nre-declares the annotation (now accepted, the sub-class method is the\none mapped), a non-annotated covariant override (the super class\nmethod is ignored) and a sub-class method that is not an override\n(different name, same payload type) which continues to be rejected as\na duplicate annotation.\n"
    },
    {
      "commit": "6e6f2fbd8dfbe5f7243a1fcf9415d3854196b902",
      "tree": "d353e367f2ad3ac34851cdc9b7f861c2390ee734",
      "parents": [
        "7cd342b4c5844741c13f95405bfb556cddf6e316"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 13:50:48 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 13:50:48 2026 +0200"
      },
      "message": "Reject @OnMessage methods with a primitive payload and boolean last flag\n\nPartial message delivery is only defined for String, byte[] and\nByteBuffer payloads, optionally with a boolean last flag. Tomcat\nalready rejects Reader, InputStream, PongMessage and decoded object\npayloads combined with a boolean last flag at deployment time but did\nnot reject the same combination for a primitive (or boxed primitive)\npayload parameter.\n\nFor a method like void onMessage(int payload, boolean last) the method\nmapping was accepted, handler creation then fell through to the partial\nByteBuffer branch and constructed PojoMessageHandlerPartialBinary with\nan index of -1. The first fragmented binary message caused\nparameters[indexPayload] \u003d message in\nPojoMessageHandlerPartialBase.onMessage() to throw an\nArrayIndexOutOfBoundsException outside the try/catch, which WsFrameBase\nhandled as an unexpected error, invoking onError() and closing the\nsession abnormally (1006). Any client connecting to such an endpoint\ncould trigger this with a single fragmented binary message.\n\nReject the parameter combination at deployment time instead, matching\nthe existing partial parameter validation, with a new\npojoMethodMapping.partialPrimitive message. This also removes the last\nroute by which the partial message handler creation could be reached\nwithout a valid payload index.\n\nAdd a regression test asserting the DeploymentException is raised for\nthis signature.\n"
    },
    {
      "commit": "7cd342b4c5844741c13f95405bfb556cddf6e316",
      "tree": "5dde0dd5173bfb42687b17dadc04b5430f19162b",
      "parents": [
        "60b5bcb0a8bd8f74d80a498416d852f9ad907912"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 13:19:33 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 13:19:33 2026 +0200"
      },
      "message": "Fix NPE handling connections to a POJO endpoint with no annotated methods\n\nThe method mapping for a POJO @ServerEndpoint class is stored in the user\nproperties of the ServerEndpointConfig only when the class actually has\nannotated methods. PojoEndpointServer.onOpen() and the onClose()/onError()\nhandling in PojoEndpointBase then dereference that mapping unconditionally,\nso the first connection to a class annotated with @ServerEndpoint but with\nno @OnOpen, @OnMessage, @OnClose or @OnError method triggered a\nNullPointerException. The error handling NPEd a second time and the\nexception escaped WsHttpUpgradeHandler.init(), failing the upgrade with an\nopaque error instead of a usable session.\n\nThe \"only if there are methods\" condition was introduced for bug 58232 to\navoid storing a mapping for programmatic endpoints (Endpoint subclasses)\nsince those never use it. That restriction was never intended to cover POJO\nendpoints, for which the mapping must always be present. Store it for any\nendpoint class that is not an Endpoint subclass, retaining the previous\nbehaviour for Endpoint subclasses, and always store the mapping created at\nupgrade time in UpgradeUtil.doUpgrade() where the surrounding condition\nalready excludes Endpoint subclasses.\n\nAn empty mapping is harmless: PojoMethodMapping performs no validation when\na class has no annotated methods and getMessageHandlers() returns an empty\nset, so such an endpoint now behaves like the equivalent @ClientEndpoint\nPOJO - the connection opens and simply has no handlers.\n\nAdd unit tests covering both addEndpoint() paths (stored for a POJO class\nwith no methods, still not stored for an Endpoint subclass, stored by\nupgradeHttpToWebSocket()) and an end-to-end test that a connection to such\nan endpoint stays open.\n"
    },
    {
      "commit": "60b5bcb0a8bd8f74d80a498416d852f9ad907912",
      "tree": "98b9ab530a03762f117f7be0b399ad536d824334",
      "parents": [
        "ec331681dbe31474373bdabf997e53eb8761b763"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 12:57:55 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 12:57:55 2026 +0200"
      },
      "message": "Fix 32-bit truncation of the WebSocket client I/O timeout\n\nThe connectToServerRecursive() method read the millisecond value of the\norg.apache.tomcat.websocket.IO_TIMEOUT_MS user property with\nLong.valueOf(...).intValue(), silently truncating it to 32 bits. A\nconfigured timeout greater than 2^31-1 ms (~24.8 days) was\nmisinterpreted: a value whose low 32 bits are negative (e.g. 30 days)\nbecame a negative timeout, causing the subsequent Future.get(timeout,\nMILLISECONDS) calls for the connection, handshake and request/response\nsteps to fail immediately, while other large values were silently\nshortened to their low 32 bits.\n\nUse longValue(). The value is only ever passed directly to\nFuture.get(long, TimeUnit.MILLISECONDS), so the restored full long range\nintroduces no deadline arithmetic issues, and behaviour for values\nwithin the previous int range is unchanged.\n"
    },
    {
      "commit": "ec331681dbe31474373bdabf997e53eb8761b763",
      "tree": "5134e9a96757c018ec1ae54a4cee0fc39e070cb2",
      "parents": [
        "3bcf5f2be743d030e08cdaa22155f9397865a252"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 12:46:36 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 12:46:36 2026 +0200"
      },
      "message": "Fix 32-bit truncation of the session close timeout\n\nWsSession.getSessionCloseTimeout() read the Long value of the\norg.apache.tomcat.websocket.SESSION_CLOSE_TIMEOUT user property with\nintValue(), silently truncating it to 32 bits. A configured timeout\ngreater than 2^31-1 ms (~24.8 days) was misinterpreted: a value whose\nlow 32 bits are negative (e.g. 30 days) was rejected by the check for a\nnon-positive value and silently replaced with the 30 second default,\nwhile other large values were silently shortened to their low 32 bits.\n\nUse longValue(), matching the sibling methods\ngetAbnormalSessionCloseSendTimeout() and getBlockingSendTimeout(). The\nclose timeout expiry is compared using the wrap-safe\nSystem.nanoTime() subtraction form, so the now usable full long range\ndoes not introduce any overflow related regression.\n"
    },
    {
      "commit": "3bcf5f2be743d030e08cdaa22155f9397865a252",
      "tree": "8463ddaff763e1c5b07706e7d9e19d5fd2cfcd52",
      "parents": [
        "820c9890651126a89aba847b19d7fc59b4991cb6"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 11:38:36 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 11:38:36 2026 +0200"
      },
      "message": "Validate the WebSocket maximum message buffer size\n\nThe session setters setMaxBinaryMessageBufferSize() /\nsetMaxTextMessageBufferSize() and the container default setters\nsetDefaultMaxBinaryMessageBufferSize() /\nsetDefaultMaxTextMessageBufferSize() accepted any int without\nvalidation.\n\nWith a zero size and a MessageHandler.Partial registered, the frame\nreceiver enters an unbounded busy loop: the terminal transformation\nalways reports OVERFLOW because the destination buffer has no room, so\nthe application handler is invoked repeatedly with empty buffers and\nthe frame payload is never consumed. A negative size results in a\nNegativeArraySizeException when the message buffer is allocated.\n\nReject values below one with an IllegalArgumentException in the four\nsetters. Since the session initializes its sizes from the container\ndefaults and the server context init parameters are applied through the\ncontainer setters, this covers all configuration paths. Add a\nregression test covering the container and session setters.\n"
    },
    {
      "commit": "820c9890651126a89aba847b19d7fc59b4991cb6",
      "tree": "b08a5e8ae6b1daddfd8cbb3f401c2b96e688cd60",
      "parents": [
        "7beac7acd5e3456095c3ec0724e7d9acb7b2ea56"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 11:18:12 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 11:18:12 2026 +0200"
      },
      "message": "Fix resource bundle key used by WsFrameClient\n\nThe defensive IllegalStateException thrown by the default branch of\nprocessSocketRead() used the message key\n\"wsFrameServer.illegalReadState\". That key is only defined in the\norg.apache.tomcat.websocket.server resource bundle. WsFrameClient\u0027s\nStringManager loads the bundle of its own package\n(org.apache.tomcat.websocket), so the lookup failed and the exception\nmessage degraded to the raw key instead of the intended\n\"Unexpected read state [X]\", making diagnosis of the (rare) read state\nrace confusing.\n\nUse the key \"wsFrame.illegalReadState\", which is defined in the\nwebsocket bundle and is already used correctly elsewhere in the same\nclass. This is a copy-paste leftover from WsFrameServer, which\nlegitimately uses the server bundle key.\n"
    },
    {
      "commit": "247a8884e1805a1361990734a6b1b5f789fb1c83",
      "tree": "54f3e214b41d9f765c93b16304b385f03359409c",
      "parents": [
        "2d6765793c0751491d35da5209a5ee676ec8606a"
      ],
      "author": {
        "name": "remm",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 10:46:21 2026 +0200"
      },
      "committer": {
        "name": "remm",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 10:46:21 2026 +0200"
      },
      "message": "Report the correct length in Asn1Parser.parseLength() error messages\n\nBased on code from PR#1090 submitted by Michael Osipov.\nAdd test produced using OpenCode."
    },
    {
      "commit": "7beac7acd5e3456095c3ec0724e7d9acb7b2ea56",
      "tree": "6f035505e710d82c5c0511e1cafcd6e5fd777062",
      "parents": [
        "45d38f87f888d6d00cca77626e78daf8a199343f"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 00:37:12 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Sat Oct 17 00:37:17 2026 +0200"
      },
      "message": "Make TransformationFactory thread-safe for concurrent registration\n\nTransformationFactory is a JVM-wide singleton. Its builders map was a\nplain HashMap mutated by the public registerExtension() method, which\nthird parties (e.g. TCK tests installing custom transformations) may\ncall at any time, while create() is invoked on every WebSocket\nhandshake from request threads and getInstalledExtensionNames()/\ngetInstalledExtensions() from client application threads. A concurrent\nput racing get/iteration on a HashMap can lose entries or throw\nConcurrentModificationException from the handshake path.\n\nUse a ConcurrentHashMap so registration and reads are safe without\nexternal coordination. A test exercising concurrent registration and\nhandshake-style reads reproduces the ConcurrentModificationException\non the previous HashMap implementation.\n"
    },
    {
      "commit": "45d38f87f888d6d00cca77626e78daf8a199343f",
      "tree": "507c8ef3a6c2cf59a4b6ccc6ae477b0b2b763760",
      "parents": [
        "933e3146c36086a0823e2827fb5ec1d3d51a5683"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 23:47:17 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 23:47:17 2026 +0200"
      },
      "message": "Report primitive message decode failures as DecodeException\n\nA @OnMessage method with a primitive (or boxed primitive) text payload\nparameter coerces the message via Util.coerceToType(), which throws\nNumberFormatException for invalid numeric input and\nStringIndexOutOfBoundsException for an empty message targeted at char.\n\nThese unchecked exceptions escaped PojoMessageHandlerWholeText.decode(),\npassed through PojoMessageHandlerWholeBase.onMessage() which only\ncatches DecodeException, and were handled by WsFrameBase as an\nunexpected failure: the endpoint\u0027s onError received the raw runtime\nexception and the connection was closed abnormally (1006).\n\nConvert the failure to a jakarta.websocket.DecodeException at the call\nsite, matching how PojoMethodMapping already wraps the same call for\npath parameters. The endpoint\u0027s onError now receives a DecodeException\nand the connection stays open, as for decoder based endpoints.\n\nAdd end-to-end regression tests covering an empty message for a char\nparameter and non-numeric text for an int parameter.\n"
    },
    {
      "commit": "933e3146c36086a0823e2827fb5ec1d3d51a5683",
      "tree": "bae240d0635e20adae2628445da7a5ea2c294857",
      "parents": [
        "886c4abb95ed72d2bf9115db00e2c8b8f6ffdee5"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 23:37:44 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 23:37:44 2026 +0200"
      },
      "message": "Reject the permessage-deflate compressed bit on continuation frames\n\nRFC 7692 section 6.1: \"An endpoint MUST NOT set the \u0027Per-Message\nCompressed\u0027 bit of control frames and non-first fragments of a data\nmessage. An endpoint receiving such a frame MUST Fail the WebSocket\nConnection.\"\n\nPerMessageDeflate.validateRsv() only enforced this for control frames.\nFor any other opcode it stripped RSV1 and delegated to the next\ntransformation, so a continuation frame carrying the bit was silently\naccepted instead of failing the connection. Route continuation frames\n(opcode 0x00, non-first fragments by definition) through the same\nreject path so WsFrameBase closes the connection with PROTOCOL_ERROR,\nas the RFC requires.\n\nTomcat\u0027s own sender sets RSV1 only on the first compressed frame of a\nmessage (firstCompressedFrameWritten), so this cannot break compressed\nfragmented messages between Tomcat endpoints.\n\nAdd a regression test to TestPerMessageDeflate covering RSV1 on text,\nbinary, continuation and control frames, plus RSV clear on continuations.\n"
    },
    {
      "commit": "886c4abb95ed72d2bf9115db00e2c8b8f6ffdee5",
      "tree": "b1268d3078122f9adfa40291f7726572e62fc6c7",
      "parents": [
        "57ec4a3967cfd181971d0d6c3c1901b857b328c1"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 23:21:41 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 23:21:41 2026 +0200"
      },
      "message": "Use UTF-8 for WebSocket client basic authentication credentials\n\nThe WebSocket client\u0027s BasicAuthenticator only selected UTF-8 when the\nserver challenge carried a charset parameter with the value UTF-8 (case\ninsensitively) and silently fell back to ISO-8859-1 for any other\nvalue, including values that RFC 7617 section 2.1 does not allow at\nall. Credentials containing characters outside ISO-8859-1 were then\nsilently corrupted to \u0027?\u0027 by the ISO-8859-1 encoding, resulting in\nmysterious authentication failures against such servers.\n\nRFC 7617 section 2.1 leaves the default encoding undefined (provided it\nis ASCII compatible) and allows only UTF-8 as the value of the charset\nparameter, reserving other values for future use. Encode the\ncredentials with UTF-8 by default and reject challenges asking for any\nother encoding rather than silently guessing one. Add a unit test that\nchecks the encoding byte-wise and the rejection of disallowed values.\n\nFound by code review.\n"
    },
    {
      "commit": "57ec4a3967cfd181971d0d6c3c1901b857b328c1",
      "tree": "1a47a76e67a1de24cb98c475b36cf1001392192d",
      "parents": [
        "2a0e4f53e157824029652cbde1c699b9fae1f921"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 23:06:23 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 23:06:23 2026 +0200"
      },
      "message": "Reject digest challenges without a realm in WebSocket client\n\nThe mandatory realm parameter of the server Digest challenge (RFC 7616\nsection 3.3) was not validated. If a non-compliant server omitted the\nrealm and the user had not configured one, the client sent an\nauthorization header containing the literal text realm\u003d\"null\" and\ncomputed the digest using the string \"null\" in A1, resulting in a\nmalformed header and a silent authentication failure.\n\nThrow an explicit AuthenticationException when the challenge omits the\nrealm, matching the existing handling of a missing qop value. Add a unit\ntest for the case.\n\nFound by code review.\n"
    },
    {
      "commit": "2a0e4f53e157824029652cbde1c699b9fae1f921",
      "tree": "41562d2cc5bf59e824cef7d34249d764b5e61f8e",
      "parents": [
        "d380d3207024fe00e3861b271e57a7b99703eebc"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 23:00:22 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 23:00:22 2026 +0200"
      },
      "message": "Fix WebSocket client digest response calculation for qop auth-int\n\nWhen the server challenge used qop\u003d\"auth-int\", the WebSocket client\u0027s\nDigestAuthenticator computed A2 as Method \":\" digest-uri-value,\nomitting the H(entity-body) term required by RFC 2617 section 3.3.2 and\nRFC 7616. A WebSocket handshake has no entity body but the hash of the\nempty string still has to be included. Without it, the generated\nresponse value was always rejected by such servers, making the client\nunable to authenticate to third-party servers that advertise auth-int.\n\nAppend the hash of the empty entity body to A2 when the challenge qop\nis auth-int and correct the comment that claimed the term was not\nneeded.\n\nAdd a unit test that checks the generated response value against an\nindependent, RFC based calculation done the same way a server would, for\nboth qop\u003dauth and qop\u003dauth-int. The auth-int case fails against the\nprevious code.\n\nFound by code review.\n"
    },
    {
      "commit": "d380d3207024fe00e3861b271e57a7b99703eebc",
      "tree": "97418b0c3f366186f4e28b8799e3fdcadb564401",
      "parents": [
        "2d6765793c0751491d35da5209a5ee676ec8606a"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 22:51:22 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 22:51:22 2026 +0200"
      },
      "message": "Honor the write timeout in AsyncChannelWrapperSecure\n\nThe array based write(...) method ignored its timeout and unit\nparameters. WriteTask.run() waited on each socket write with an\nunbounded Future.get() so, if the peer stopped accepting data, the\nwrite thread blocked forever, the writing flag was held (blocking all\nsubsequent writes) and the caller\u0027s completion handler never completed.\n\nCompute a deadline from the requested timeout when the write task is\ncreated, with the AsynchronousSocketChannel semantics that a timeout of\nzero or less means wait indefinitely. Each socket write now waits only\nfor the remaining time. On expiry, the pending channel write is\ncancelled and the future is failed with a TimeoutException through the\nexisting error handling, which releases the writing flag and notifies\nthe completion handler. The single buffer write(ByteBuffer) path, which\nhas no timeout parameter, is unchanged.\n\nFound by code review.\n"
    },
    {
      "commit": "2d6765793c0751491d35da5209a5ee676ec8606a",
      "tree": "fa390afc2676abb575710b90785777f2fcac0d0a",
      "parents": [
        "833f3db6e00fff38cc0f8c163ad8ab9a2087bc32",
        "54dfd76a47da183ac45d661d8637545c7ba8f436"
      ],
      "author": {
        "name": "remm",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 16:59:47 2026 +0200"
      },
      "committer": {
        "name": "remm",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 16:59:47 2026 +0200"
      },
      "message": "Merge branch \u0027main\u0027 of git@github.com:apache/tomcat.git into main"
    },
    {
      "commit": "54dfd76a47da183ac45d661d8637545c7ba8f436",
      "tree": "8055da516b57b7e90da63869a42278e615db4420",
      "parents": [
        "764112d92298a70dcb1c6ed9d9b8a034a4c66390"
      ],
      "author": {
        "name": "remm",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 15:44:14 2026 +0200"
      },
      "committer": {
        "name": "remm",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 15:44:14 2026 +0200"
      },
      "message": "A test implementation now exists allowing further research"
    },
    {
      "commit": "833f3db6e00fff38cc0f8c163ad8ab9a2087bc32",
      "tree": "d04c02aa7831d55acecf9ed695e71baba3bac441",
      "parents": [
        "5b21604bf0fc4556ae78ce753ada02d53fd4d73f"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 15:31:00 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 15:31:00 2026 +0200"
      },
      "message": "Shut down the internal virtual thread executor when the endpoint stops\n\nshutdownExecutor() only shut down the thread pool branch, leaving a\nvirtual thread executor accepting tasks submitted through a reference\ncaptured before the stop. Shut it down as well, so such submissions\nare rejected, as they already are with the thread pool executor.\n"
    },
    {
      "commit": "5b21604bf0fc4556ae78ce753ada02d53fd4d73f",
      "tree": "d562e2de0e9b21290a82c9b3a261f59207216b32",
      "parents": [
        "9d72ce685af8c1f69436688897283f94141be84b"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 15:25:50 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 15:25:50 2026 +0200"
      },
      "message": "Fix submitted task count drift in ThreadPoolExecutor\n\nAdjust the submitted task counter on every path where a counted task\ncan no longer reach afterExecute(): a null command, a rejection\nhandler that does not throw, removal of a queued task via remove() or\npurge(), and a beforeExecute override that throws. The counter feeds\nTaskQueue\u0027s queue-or-create-thread decision, so a drifted value makes\nidle pools create unnecessary threads.\n"
    },
    {
      "commit": "764112d92298a70dcb1c6ed9d9b8a034a4c66390",
      "tree": "82cb1ede5d3d64c17766a876a34838adbd13428a",
      "parents": [
        "8ebd9f3423a2a32db884fed583fb6f6588987e5c"
      ],
      "author": {
        "name": "Dimitris Soumis",
        "email": "jimsoumis@gmail.com",
        "time": "Fri Oct 16 00:54:02 2026 +0300"
      },
      "committer": {
        "name": "Dimitrios Soumis",
        "email": "jimsoumis@gmail.com",
        "time": "Fri Oct 16 14:57:59 2026 +0200"
      },
      "message": "Add PR diff-coverage workflow (report-only)\n"
    },
    {
      "commit": "8ebd9f3423a2a32db884fed583fb6f6588987e5c",
      "tree": "061e83cacbb957a303ca80a826ef928592d4bba2",
      "parents": [
        "f687aee436b76130c65a64a1ed863bfe23cafa5a"
      ],
      "author": {
        "name": "Dimitris Soumis",
        "email": "jimsoumis@gmail.com",
        "time": "Thu Oct 15 20:30:54 2026 +0300"
      },
      "committer": {
        "name": "Dimitrios Soumis",
        "email": "jimsoumis@gmail.com",
        "time": "Fri Oct 16 14:57:59 2026 +0200"
      },
      "message": "Add JaCoCo XML output to the coverage-report target\n"
    },
    {
      "commit": "9d72ce685af8c1f69436688897283f94141be84b",
      "tree": "b66f99252e106dbf1c23c7b9496701990052c024",
      "parents": [
        "f358a09a31be76a03bcda94938b6b99c7ed3e02a"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 14:48:24 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 14:48:24 2026 +0200"
      },
      "message": "Fix premature termination of InlineExecutorService\n\nTrack the number of running tasks with a counter instead of a single\nboolean flag. The executor now reports itself terminated only when a\nshutdown has been requested and no task is still running, which keeps\nawaitTermination() from returning while a task is in progress and\nallows shutdown() to run tasks queued by a running task. shutdown()\nalso notifies waiting threads when it declares termination.\n"
    },
    {
      "commit": "f358a09a31be76a03bcda94938b6b99c7ed3e02a",
      "tree": "1019d850657cc4e78bb5d3758853be345f2a7b62",
      "parents": [
        "a37307ea45a595010996c66f313ae64c4113ef6f"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 14:28:58 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 14:28:58 2026 +0200"
      },
      "message": "Keep the acquisition count of LimitLatch stable across releaseAll()\n\nreleaseAll() wakes the queued threads by releasing the synchronizer.\nThe release path also decremented the acquisition counter by one, a\ndecrement that corresponded to no thread returning a share, leaving the\ncounter below the number of shares actually held. Callers that inspect\nthe counter after releaseAll(), or that continue to use the latch\nrather than discarding it, saw fewer acquisitions counted than there\nwere, so the limit could be exceeded once enforced again.\n\nreleaseAll() now wakes the waiting threads without changing the\ncounter. Threads released in this way hold a share until they call\ncountDown(), like any other acquisition. The documentation of reset()\nnow describes the consequence of resetting while shares are still held\nand that queued threads are not woken by a reset, and the documented\nreturn value of countDown() now matches the value it has always\nreturned.\n\nAdd testReleaseAllKeepsCount which pins the counter through a\nreleaseAll() cycle with waiting threads.\n"
    },
    {
      "commit": "a37307ea45a595010996c66f313ae64c4113ef6f",
      "tree": "722809b0dac307a7ba59fa6915e6daa7149fcf3d",
      "parents": [
        "12f83a700e3630c7e0710041a1c463a0ff135cf9"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 14:21:04 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 14:21:04 2026 +0200"
      },
      "message": "Avoid busy spin in TaskQueue.take() when the keep-alive time is zero\n\nA worker whose thread is due to be stopped waits with a timed poll so\nthat the check for whether the thread should stop runs periodically.\nThe poll used the pool keep-alive time as the timeout. That time can be\nconfigured to zero, in which case the poll can not block and, for a\ncore thread that is not subject to being culled, the wait degenerates\ninto a loop that consumes a full CPU core while the queue is empty.\n\nWhen the configured timeout rounds down to zero, wait for the thread\nrenewal delay instead. The interval at which a thread may stop itself is\ngated by that same delay, so the check still runs at a bounded interval\nwithout the spin, arriving tasks are still picked up immediately and\nthe wait remains interruptible. Timed waits for non-core workers, which\nrely on a zero keep-alive time to be culled immediately, are not\naffected.\n\nAdd TestTaskQueue with a regression test that counts the poll\niterations performed by idle, renewal-due workers. Without the bounded\ntimeout the count reaches millions within the test window.\n"
    },
    {
      "commit": "12f83a700e3630c7e0710041a1c463a0ff135cf9",
      "tree": "c84d504bd0c791b7b4530da9895afe922debc90f",
      "parents": [
        "2ff4bbdab4748c80ca2fe083f77e177db9797d00"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 14:08:26 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 14:08:26 2026 +0200"
      },
      "message": "Require at least one iteration when computing message digests\n\nConcurrentMessageDigest.digest(String, int, byte[]...) documents the\niterations argument as the number of times the digest is applied, but\nthe value was not checked. A count of zero or less still produced the\ndigest of the input, because the first round runs unconditionally and\nthe additional rounds only run when the count is greater than one.\nCallers that passed such a count silently received a result that did\nnot match the documented semantics.\n\nThe method now throws IllegalArgumentException when fewer than one\niteration is requested, which is consistent with how the JDK validates\nthe iteration count of PBEKeySpec.\n\nStored credentials used by the message digest credential handler carry\ntheir own iteration count. A stored count below one was previously\naccepted and silently verified as a single iteration. Such credentials\nare now reported as invalid, matching the existing handling of other\nmalformed stored credentials, so verification returns false instead of\nusing an iteration count the digest computation rejects.\n\nAdd regression tests covering the rejected counts, repeated iteration\nsemantics and stored credentials with an invalid iteration count.\n"
    },
    {
      "commit": "2ff4bbdab4748c80ca2fe083f77e177db9797d00",
      "tree": "1944399720a038f230905d8be4ad2923206a0ac0",
      "parents": [
        "41b328066eb397119215f9daeccfebdef0626e79"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 13:51:30 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 13:51:30 2026 +0200"
      },
      "message": "Report the resource path when a WEB-INF/lib JAR cannot be scanned\n\nWhen scanning WEB-INF/lib, the debug message, the warning for a null\nresource and the trace message for a skipped JAR all identify the JAR\nby its web application relative path. The warning for an IOException\nused the URL local variable instead. That variable is still null if it\nis the getResource() call itself that fails, and a full URL is\nredundant in a message that already states the JAR came from\n/WEB-INF/lib. Use the path in this message as well so every log\nmessage from the loop identifies the JAR the same way.\n"
    },
    {
      "commit": "41b328066eb397119215f9daeccfebdef0626e79",
      "tree": "a62c133715e3b4d37ae61311fb0575ad7ab8ee44",
      "parents": [
        "7a6519f9f165a38578d142bd316123b8bcd0a709"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 13:43:25 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 13:43:25 2026 +0200"
      },
      "message": "Resolve nested JAR entries in JarFileUrlNestedJar using the JarURLConnection\n\nThe constructor previously derived the entry name for the nested JAR by\nmanually extracting the text after the \"!/\" separator from the URL\nstring. Percent-encoding in the entry name is not visible in the URL\nstring in decoded form, so for URLs that named the entry using percent\nencoding (for example a nested JAR whose file name contains a space),\nthe manual lookup did not find the entry the JDK itself resolves the\nURL to. The Jar was then constructed with no entry, and the problem\nonly surfaced later as a NullPointerException when the content of the\nJar was first accessed.\n\nThe entry is now resolved with JarURLConnection.getJarEntry(), which\napplies the same decoding the JDK applies when it resolves the URL.\nThis is the same approach JarFileUrlJar already uses. If the URL does\nnot name an entry at all (it ends with \"!/\"), a FileNotFoundException\nis now thrown when the Jar is constructed rather than leaving the Jar\nin a state that fails unexpectedly later.\n\nAdd TestJarFileUrlNestedJar to cover plain and percent-encoded entry\nnames as well as the missing entry, empty entry name and missing\nseparator cases.\n"
    },
    {
      "commit": "f687aee436b76130c65a64a1ed863bfe23cafa5a",
      "tree": "8e6b6c417253391caa0cf1a4588662904832ac2c",
      "parents": [
        "65ec6141033661fffd238e42fe401aadbecc01a2"
      ],
      "author": {
        "name": "Mark Thomas",
        "email": "markt@apache.org",
        "time": "Fri Oct 16 12:40:35 2026 +0100"
      },
      "committer": {
        "name": "Mark Thomas",
        "email": "markt@apache.org",
        "time": "Fri Oct 16 12:40:35 2026 +0100"
      },
      "message": "Add credit to the changelog"
    },
    {
      "commit": "65ec6141033661fffd238e42fe401aadbecc01a2",
      "tree": "d79b0e705c26a2fed71a936c5ccabd46b93ccec3",
      "parents": [
        "87f1a692fe028ddb1a908ecea095e13effcc7257"
      ],
      "author": {
        "name": "Angelo Rubens",
        "email": "angelorubini@hotmail.it",
        "time": "Mon Oct 12 00:44:12 2026 +0200"
      },
      "committer": {
        "name": "Mark Thomas",
        "email": "mark.thomas@broadcom.com",
        "time": "Fri Oct 16 12:39:22 2026 +0100"
      },
      "message": "Hold the standalone identifier marker in the EvaluationContext\n\nAstIdentifier sets ELResolver.StandaloneIdentifierMarker for every identifier\nit resolves (TRUE for a stand-alone identifier, then back to FALSE). Holding it\nin the EvaluationContext, which is what the resolvers are passed, rather than\nin the map of the wrapped ELContext saves the map updates of every identifier.\nWith Mojarra\u0027s test/perf pages, this lowers the CPU time per request by about\n2%.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) \u003cnoreply@anthropic.com\u003e\n"
    },
    {
      "commit": "87f1a692fe028ddb1a908ecea095e13effcc7257",
      "tree": "750871a502813bdc731d66fab01a2a906e471724",
      "parents": [
        "9649e5fc9da9adffdf46fb97abcf3f642c3b4111"
      ],
      "author": {
        "name": "Raphael Vullriede",
        "email": "raphael@osslabz.net",
        "time": "Fri Oct 16 13:26:15 2026 +0200"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Oct 16 12:26:15 2026 +0100"
      },
      "message": "Fix BZ 70269 - StatementCache regression with drivers that report statements as not poolable (#1083)\n\nhttps://bz.apache.org/bugzilla/show_bug.cgi?id\u003d70269\n\nCo-authored-by: Mark Thomas \u003cmarkt@apache.org\u003e"
    },
    {
      "commit": "7a6519f9f165a38578d142bd316123b8bcd0a709",
      "tree": "c6609ebe4cd348e428e4b297be208faa4dc6b534",
      "parents": [
        "2a3b326b53927b6b880a539a342a95d5ac1e4ee8"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 13:22:07 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 13:22:07 2026 +0200"
      },
      "message": "Ignore extra close() calls in ReferenceCountedJar\n\nAn extra close() call (more closes than opens) drove the reference count\nbelow zero. Since the wrapped Jar is only closed when a balanced close\nreturns the count to exactly zero, once the count went negative no\nsubsequent open()/close() cycle would close the wrapped Jar again,\nleaking it and its file handle until the garbage collector ran the\ncleaner. Extra close() calls are now ignored, saturating the count at\nzero. Current callers all balance their references, so this hardens the\npublic API rather than fixing an active leak.\n"
    },
    {
      "commit": "2a3b326b53927b6b880a539a342a95d5ac1e4ee8",
      "tree": "b818090f89486e444fa760c0af62b5bafecae2ca",
      "parents": [
        "755ccb930d8471f313c4d8eb8ffbb98a096fe9ad"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 13:16:24 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 13:16:24 2026 +0200"
      },
      "message": "Keep streams returned from ReferenceCountedJar open\n\nThe stream-returning methods released the reference they opened before\nthe caller could use the returned stream, so when no other reference was\nheld the wrapped JAR was closed and the stream could no longer be read.\nThey now transfer the opened reference to the returned stream: it is\nreleased when the stream is closed, exactly once, so the wrapped JAR\nstays open for the lifetime of the stream. This matches the Jar contract\nunder which the caller is responsible for closing streams. A null stream\nand failed look-ups release the reference immediately.\n"
    },
    {
      "commit": "9649e5fc9da9adffdf46fb97abcf3f642c3b4111",
      "tree": "dc680f2e1689a292daad954f736831cbf8e43bba",
      "parents": [
        "fe0b1e8731479b8f31249c3434a99f33cc84f5ae"
      ],
      "author": {
        "name": "Mark Thomas",
        "email": "markt@apache.org",
        "time": "Fri Oct 16 12:06:12 2026 +0100"
      },
      "committer": {
        "name": "Mark Thomas",
        "email": "markt@apache.org",
        "time": "Fri Oct 16 12:06:12 2026 +0100"
      },
      "message": "Update changelog"
    },
    {
      "commit": "fe0b1e8731479b8f31249c3434a99f33cc84f5ae",
      "tree": "3e26f528e9e6da72dc5c9d0aa3b7469ddd4cdd08",
      "parents": [
        "de25bedd07f259642501bdaeb998ba0bb635dbd0"
      ],
      "author": {
        "name": "Raphael",
        "email": "raphael@osslabz.net",
        "time": "Wed Oct 14 10:51:47 2026 +0200"
      },
      "committer": {
        "name": "Mark Thomas",
        "email": "mark.thomas@broadcom.com",
        "time": "Fri Oct 16 12:02:14 2026 +0100"
      },
      "message": "Fix BZ 68436 - don\u0027t warn when driverClassName is not set\n\nhttps://bz.apache.org/bugzilla/show_bug.cgi?id\u003d68436\n"
    },
    {
      "commit": "755ccb930d8471f313c4d8eb8ffbb98a096fe9ad",
      "tree": "04c1e7bae3496defa14c0c40f1236f62998c9b9c",
      "parents": [
        "c8f4c0dc8da01d54cb01c5451ddb68de71089383"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 12:58:13 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 12:58:13 2026 +0200"
      },
      "message": "Correct the selection of multi-release JAR entries in JarFileUrlJar\n\nThe entry selected for a class present with multiple versions depended\non the order in which entries appeared in the JAR: a versioned entry\nencountered first pinned the selection, so an older version could be\nused, and an entry that only exists for a later runtime version could\nbe selected although the runtime would never use it. The selection now\nbuilds a map of the highest applicable versioned entry for each base\nclass name, mirroring the stream-based implementation, and resolves\nevery first-seen base name through it, so the scanned entry matches the\nentry the runtime would use regardless of entry order. Versioned\nnon-class entries are resolved to the base entry, as the stream-based\nimplementations already do, and entries only applicable to a later\nruntime are skipped.\n"
    },
    {
      "commit": "de25bedd07f259642501bdaeb998ba0bb635dbd0",
      "tree": "efdb34809b1ff63f0f6c7feebdcf90cbba70bee8",
      "parents": [
        "ee868c8efdceb6332dd64776083d0c8c50368e4a"
      ],
      "author": {
        "name": "Mark Thomas",
        "email": "markt@apache.org",
        "time": "Fri Oct 16 11:35:24 2026 +0100"
      },
      "committer": {
        "name": "Mark Thomas",
        "email": "markt@apache.org",
        "time": "Fri Oct 16 11:35:24 2026 +0100"
      },
      "message": "Follow-up to \"Validate the principal object graph before persisting...\"\n\nSee the diff for the full explanation. The short version is that this\nwas an expensive check paid by everyone for a rare bug that should\naffect very few - hence the refactoring."
    },
    {
      "commit": "c8f4c0dc8da01d54cb01c5451ddb68de71089383",
      "tree": "26a7c6a63b3e109b81cf3cc307ad4be53b3573f2",
      "parents": [
        "68f7334f08ffd5092bc36336e614884b9e7fc25d"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 11:55:30 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 11:55:30 2026 +0200"
      },
      "message": "Harmonize resource bundle processing in ProxyErrorReportValve\n\nThe redirect url lookup of ProxyErrorReportValve passed the response\nlocale straight to ResourceBundle.getBundle(). The StringManager locale\nprocessing introduced by commit c45c9b47f3 handles this differently: a\nbundle is only loaded for a locale for which a bundle actually exists,\nchecked with a cache free probe.\n\nAlign the valve with that processing. Resolve the response locale\nthrough the candidate locales of the default control, check each\ncandidate with StringManager.bundleExists() and only then load the\nbundle for the first matching candidate. Locales that match no\ncandidate fall back to the default error report as before. To allow the\nreuse, make bundleExists() in StringManager accessible outside the\nclass. For the documented layout, a properties file at the root of the\nclass path, the resolution is unchanged for every locale.\n"
    },
    {
      "commit": "68f7334f08ffd5092bc36336e614884b9e7fc25d",
      "tree": "197df051d2e5ee3164ff8c3d4c4fe01ce4f10a57",
      "parents": [
        "4d25a9923bad13b399029ecf2ebca3e2443c7b64"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 11:28:13 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 11:28:13 2026 +0200"
      },
      "message": "Bind the nonce value parameter of OCSP_request_add1_nonce as a pointer\n\nThe C prototype is int OCSP_request_add1_nonce(OCSP_REQUEST *req,\nunsigned char *val, int len) but the FFM binding declared the second\nparameter with the JAVA_CHAR layout and the wrapper method accepted a\nJava char. A scalar is placed where the native code reads a pointer. The\nonly call site passed (char) 0 which currently works out to a NULL\npointer through zero-extension, but the binding was formally incorrect,\na non-zero value would have produced an invalid pointer for OpenSSL to\ncopy from, and the documented use of passing an explicit nonce value was\nimpossible with this signature.\n\nBind the parameter as a pointer, accept a MemorySegment in the wrapper\nand pass MemorySegment.NULL at the call site, as intended for generating\na random nonce. The previous incorrect layout stemmed from hand-editing\nof the generated file; the corrected binding is what jextract itself\ngenerates for this prototype. No behavior change on the existing call\npath.\n"
    },
    {
      "commit": "4d25a9923bad13b399029ecf2ebca3e2443c7b64",
      "tree": "af7a2e71788330c9876f68f82bb77bce5eba3bb7",
      "parents": [
        "e4f0eec3ee8a49ce64cf85dd7e6fed4d30d9429d"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 11:14:59 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 11:14:59 2026 +0200"
      },
      "message": "Correct the OpenSSL 3 version boundary in the FFM compatibility layer\n\nOpenSSL 3.0.0 (and its pre-releases) report a version number of\n0x30000000L, since every release of the 3.x and 4.x series uses a zero\nstatus nibble in the synthesised 0xMNN00PP0L version number. The\ncompatibility code used a boundary of 0x3000000fL, a value based on the\nOpenSSL 1.x convention of released versions ending in 0xf, so exactly\nOpenSSL 3.0.0 was classified as an OpenSSL 1.x library.\n\nOn such a system the FIPS handling called FIPS_mode(), a symbol no\nOpenSSL 3.x library exports, failing library initialisation with an\nUnsatisfiedLinkError; the same applied to FIPS_mode_set() during\ndestroy(). The peer certificate retrieval used SSL_get_peer_certificate,\nwhich is not an exported symbol in 3.x, so retrieving client\ncertificates failed as well. Engines were also set up and the\nimplementation was reported as OpenSSL 1.x.\n\nUse 0x30000000L as the boundary between the 1.x and 3.x behaviour at all\nthree places the constant appeared. The affected code branches are the\nones every OpenSSL version from 3.0.1 onwards already uses, and all\nsymbols those branches reference are available in OpenSSL 3.0.0. No\nother version falls between the old and the new boundary.\n"
    },
    {
      "commit": "e4f0eec3ee8a49ce64cf85dd7e6fed4d30d9429d",
      "tree": "922441c85ade294f0bd2d1e19d9e72c96340b324",
      "parents": [
        "84aa43ef61064d0ced5c02098b24ec2298113cd7"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 10:59:05 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 10:59:05 2026 +0200"
      },
      "message": "Check for NULL after SSL_CTX_new() and SSL_new() in the OpenSSL FFM code\n\nThe SSL context and SSL object returned by SSL_CTX_new() and SSL_new()\nare passed directly to subsequent OpenSSL calls. Those calls dereference\ntheir arguments natively, so if an allocation failed, for example under\nmemory exhaustion, the JVM would crash with a SIGSEGV instead of\nreporting an error.\n\nCheck the result of each call in the panama OpenSSLContext constructor,\nthe panama OpenSSLEngine constructor and OpenSSLLibrary.findCiphers()\nand throw an exception carrying the OpenSSL error queue content when\nNULL is returned. The findCiphers() failures are reported by the\nexisting handling for that method: a logged warning and an empty cipher\nlist. In addition, reject a NULL SSL context segment in the engine\nconstructor, matching the check the JNI implementation performs.\n\nThe JNI/tomcat-native implementation effectively performs the same\nchecks: its native allocation helpers throw a Java exception when the\nunderlying OpenSSL allocation returns NULL.\n"
    },
    {
      "commit": "84aa43ef61064d0ced5c02098b24ec2298113cd7",
      "tree": "03dd4a47f2701c7c6274391e9cfe0b8778cd2a75",
      "parents": [
        "db5c4e163c8f3350cb4ad3db1fe1f7163fd0912c"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 10:47:53 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 10:47:53 2026 +0200"
      },
      "message": "Correct the FILE check for SSL_CONF commands in the OpenSSL FFM code\n\nThe checkConf() method of the panama OpenSSLContext rejected an\nSSL_CONF_TYPE_FILE command only when the configured path was neither a\nfile nor readable, i.e. when both conditions failed. As a result, a\nreadable directory or an unreadable regular file passed the check and\nthe invalid value was only rejected later, when SSL_CONF_cmd tried to\nload it, reporting a raw OpenSSL error instead of the clear\nopensslconf.badFile message.\n\nUse the intended \u0027not a file or not readable\u0027 test so that only a\nreadable regular file is accepted. This matches the behaviour of the\nJNI/tomcat-native implementation, whose path_check() requires the path\nto exist and to be a regular file. Symbolic links to readable regular\nfiles continue to pass, since File.isFile() and File.canRead() resolve\nlinks just like apr_stat() does on the JNI side.\n"
    },
    {
      "commit": "db5c4e163c8f3350cb4ad3db1fe1f7163fd0912c",
      "tree": "e3990ff7b0c294d2d25bdd040888f0382f27e570",
      "parents": [
        "2ad9b5d4850668df0ba177a59f2fb9fe54768eee"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 10:43:03 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 10:43:03 2026 +0200"
      },
      "message": "Validate the protocol of OCSP URLs from a client certificate AIA extension\n\nThe FFM OpenSSL implementation accepted any protocol that has a URL\nstream handler registered in the JVM (for example file: or jar:) when\nbuilding OCSP requests from the URLs found in the OCSP No Check AIA\nextension of a client certificate. Setting requestAttempted to true\nbefore the URLConnection was cast to HttpURLConnection meant that the\nresulting ClassCastException was reported as a non-soft-failable\nX509_V_ERR_APPLICATION_VERIFICATION error, causing the handshake to\nfail even when ocspSoftFail was enabled (the default).\n\nNow the protocol is checked before a request is attempted. A URL with\na protocol other than http or https is logged as invalid and skipped,\nmatching the handling of syntactically invalid URLs at the same place\nand the behaviour of the JNI/tomcat-native implementation, where\nOSSL_HTTP_parse_url() rejects non-HTTP URLs and no request is made.\n\nThe other elements of the original report were rejected: the DER\nlength claims used by the OCSP URL parser are bounded by\nAsn1Parser.parseLength() against the real remaining data before any\narray is allocated, so no oversized allocation is possible; and\nbounding the number of responder URLs or the aggregate wait time would\nchange the behaviour of the opt-in OCSP feature beyond what the JNI\nimplementation offers for an exposure limited to certificates signed\nby trusted CAs.\n"
    },
    {
      "commit": "2ad9b5d4850668df0ba177a59f2fb9fe54768eee",
      "tree": "9eb65b3d2467e80ad3ae67d2c383c515f84bff33",
      "parents": [
        "10c9cb1d9c250823a77c22c02f7a3bab3c652165"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 10:12:00 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 10:12:00 2026 +0200"
      },
      "message": "Account for the array offset when copying between OpenSSL FFM engine buffers and heap ByteBuffers\n\nThe write and read helper methods of the FFM based OpenSSL SSLEngine\nused the buffer position directly as the index into the array returned\nby ByteBuffer.array(). For a heap buffer that is a view of a larger\narray, for example the result of slicing the buffer at a non-zero\nposition, the index into the backing array is the array offset plus\nthe position. The unadjusted position caused the engine to read from\nand write to the wrong offset, silently corrupting the application\ndata of any wrap or unwrap call that used such a buffer.\n\nThe buffers Tomcat itself passes to the engine are all direct or have\na zero array offset, so this went unnoticed internally. It is a\nregression against the JNI based implementation, which uses ByteBuffer\nget and put methods that take the array offset into account\nimplicitly.\n"
    },
    {
      "commit": "10c9cb1d9c250823a77c22c02f7a3bab3c652165",
      "tree": "3dd18a578fb5b6df256fb2d6331bdc082b47f566",
      "parents": [
        "36a181413a4d601937fbeafd58734bd32fcc370f"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 09:22:08 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 09:22:08 2026 +0200"
      },
      "message": "Correct the cipher authentication method computation of the OpenSSL FFM implementation\n\ngetCipherAuthenticationMethod() compared the key exchange and\nauthentication values returned by SSL_CIPHER_get_kx_nid() and\nSSL_CIPHER_get_auth_nid(), which are NIDs, against the legacy OpenSSL\nSSL_k* and SSL_a* flag constants. Only NID_kx_rsa could ever match, so\na custom X509TrustManager received UNKNOWN as the authentication type\nfor virtually every cipher suite.\n\nThe legacy constants trace back to the algorithm_mkey/algorithm_auth\nbitmask table of OpenSSL 1.0.x ssl_locl.h, as also carried by the\nLibreSSL compatibility branch of Tomcat Native. They were copied into\nthe FFM port with the hex base dropped (0x10 became 10, 0x40 became 40,\nand so on) and then compared against NIDs, so they could not match\nanything.\n\nThe constants now hold the real NID_kx_rsa, NID_kx_ecdhe, NID_kx_dhe,\nNID_auth_rsa, NID_auth_ecdsa, NID_auth_dss and NID_auth_null values,\nhard-coded because these macros are not present in the generated\nbindings, and the switch now mirrors SSL_CIPHER_authentication_method()\nof Tomcat Native byte for byte, including the UNKNOWN results for\ncombinations without a JSSE authentication type such as the TLS 1.3\nkx-any value.\n"
    },
    {
      "commit": "36a181413a4d601937fbeafd58734bd32fcc370f",
      "tree": "050faa61c017cabf682fa9f04b81f492e4814ba3",
      "parents": [
        "f0ac95d70db91fd7760c3e2570b0149a43852e9c"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 08:54:06 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 08:54:06 2026 +0200"
      },
      "message": "Exclude unimplemented SSLv2 ciphers when converting OpenSSL cipher names to JSSE names\n\nopenSSLToJsse() resolved an OpenSSL cipher name only when its alias\nmapped to exactly one Cipher constant, and returned that constant\u0027s\nfirst JSSE name otherwise. Two consequences of including the\nunimplemented SSLv2 constants of the Cipher enumeration in that\nresolution:\n\n- Genuine OpenSSL names shared with an SSLv2 constant (RC4-MD5,\n  EXP-RC4-MD5, EXP-RC2-CBC-MD5) were ambiguous and resolved to\n  null. The JNI SSLEngine implementation then added that null to the\n  set returned by getSupportedCipherSuites(), violating the SSLEngine\n  contract, and the Panama findCiphers() method could add null to the\n  list later parsed by the cipher configuration parser, causing a null\n  pointer exception when a PROFILE\u003d expression resolved to one of these\n  ciphers.\n- Aliases matching only an SSLv2 constant (RC2-CBC-MD5,\n  DES-CBC3-MD5, the SSL_CK_*/SSL2_* constants) resolved to\n  non-standard pseudo JSSE names derived from the enumeration.\n\nThe conversion now excludes SSLv2 ciphers uniformly. Names shared with\nan SSLv2 constant resolve to the JSSE name of the modern cipher, names\nmatching only SSLv2 ciphers, genuinely ambiguous names and unknown\nnames resolve to null, and the two engine call sites that previously\ncould propagate null now filter unmapped names.\n"
    },
    {
      "commit": "f0ac95d70db91fd7760c3e2570b0149a43852e9c",
      "tree": "c959dd2b9c3d4e8d65f5c2bf6aa9caf8ba5c5f72",
      "parents": [
        "1f039fd41ba0fb3c4e12074b04ceadccd38c2b4b"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 08:32:57 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 08:32:57 2026 +0200"
      },
      "message": "Match OpenSSL semantics for AND cipher expressions with unknown terms\n\nIn the OpenSSL cipher configuration parser, an AND expression (X+Y+Z)\nthat contains an unknown term is only evaluated when every term is a\nknown alias. OpenSSL rejects such an element entirely (the equivalent\ncipher list string produces no cipher match).\n\nPreviously, the Java parser only required the first term to be a known\nalias and silently skipped any subsequent unknown term. This made the\nresult position dependent and silently widened the configured cipher\nset, e.g. parse(\"SHA1+Nonsense\") returned the full SHA1 set while\nparse(\"Nonsense+SHA1\") returned no ciphers at all. A typo in the\nsecond or later term of an AND expression therefore loosened the\nconfigured cipher set with no indication in the logs.\n\nThe parser now logs the unknown element as a warning and discards the\nwhole element, matching OpenSSL behavior regardless of the position of\nthe unknown term.\n\nTestCipher builds synthetic NAME+PROTOCOL tokens from the columns of\nopenssl ciphers -v output for its mapping checks. The SSLv3 protocol\nsuffix is no longer a keyword the parser supports (removed earlier to\nalign with the OpenSSL development branch) and previously relied on the\nlenient AND handling. The protocol suffix is informational for these\ntokens, so the test now strips a trailing +SSLv3 suffix before parsing\nwhile continuing to use the full token for the unmapped-cipher\nbookkeeping. A new test asserts an AND expression with an unknown term\nparses to an empty set for both term orderings.\n"
    },
    {
      "commit": "1f039fd41ba0fb3c4e12074b04ceadccd38c2b4b",
      "tree": "cf09eedd1626abd412ce8399029ddf1b4277b577",
      "parents": [
        "8379f1a20a078f21fa67bcc1fb91a79e3611c146"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 07:59:20 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 07:59:20 2026 +0200"
      },
      "message": "Make the lazily parsed wrapped certificate field volatile in OpenSSLX509Certificate\n\nOpenSSLX509Certificate lazily parses its final DER bytes into a JDK\nX509Certificate in the unsynchronized unwrap() method. Instances are\ncached in the engine peer-certificate array which is handed out\nrepeatedly to potentially concurrent application threads through\nSSLSession.getPeerCertificates() and getPeerPrincipal().\n\nWithout volatile there is no happens-before edge from the parse in one\nthread to a read in another, so a thread may never observe the parsed\ncertificate and re-parse the DER bytes on every call, and in principle\ncould observe a partially initialized certificate object. Marking the\nfield volatile makes the lazy publication safe; the remaining benign\ncheck-then-act race is an idempotent redundant parse, matching the\nexisting lock-free design. Applied to both the JNI and the Panama\nimplementations.\n"
    },
    {
      "commit": "8379f1a20a078f21fa67bcc1fb91a79e3611c146",
      "tree": "4d194d8ac73d6ccbcbbae2c7e07bf268723b902b",
      "parents": [
        "ee868c8efdceb6332dd64776083d0c8c50368e4a"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 07:56:28 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Fri Oct 16 07:56:28 2026 +0200"
      },
      "message": "Account for TLS 1.3 in the enabled protocol management of the JNI OpenSSL SSLEngine\n\nThe Tomcat Native based SSLEngine implementation advertised TLS 1.3 as\nan implemented protocol (OpenSSL 1.1.1+) but never accounted for it in\nits enabled-protocol bookkeeping:\n\n- getEnabledProtocols() only inspected the SSL_OP_NO_SSLv3, NO_TLSv1,\n  NO_TLSv1_1 and NO_TLSv1_2 options, so it always omitted TLSv1.3 from\n  the returned list even when TLS 1.3 was enabled. Since the base\n  SSLEngine.getSSLParameters() is built from getEnabledProtocols(), web\n  applications using the SSLEngine request attribute saw an incomplete\n  list.\n- setEnabledProtocols() had no TLSv1.3 case and never set the\n  SSL_OP_NO_TLSv1_3 option, so a requested protocol list excluding\n  TLS 1.3 silently left TLS 1.3 enabled.\n\nFix both by mirroring the Panama implementation which already handles\nTLS 1.3 in these places, using the existing SSL.SSL_OP_NO_TLSv1_3\nconstant. This complements the equivalent fix recently applied to the\nOpenSSLContext init reconfigure block.\n"
    },
    {
      "commit": "ee868c8efdceb6332dd64776083d0c8c50368e4a",
      "tree": "0a525b4a4c6125858936dcba1a2c70e91a7a89ce",
      "parents": [
        "46a9b901086bf44d011e3bee3c24853738660d8f"
      ],
      "author": {
        "name": "Dimitris Soumis",
        "email": "jimsoumis@gmail.com",
        "time": "Thu Oct 15 18:48:46 2026 +0300"
      },
      "committer": {
        "name": "Dimitrios Soumis",
        "email": "jimsoumis@gmail.com",
        "time": "Fri Oct 16 00:32:11 2026 +0300"
      },
      "message": "Ensure the Tomcat Native based OpenSSL SSLContext does not drop TLS 1.3 when rebuilding the list of enabled protocols during initialization.\n"
    },
    {
      "commit": "46a9b901086bf44d011e3bee3c24853738660d8f",
      "tree": "cd06457178134c3bd0962892ff4798daabcddb42",
      "parents": [
        "9f023a755b788ba72e90ffff7df4334c51457b86",
        "c5e83991e0ac7fdc77710ef22ce59b3ed859c166"
      ],
      "author": {
        "name": "remm",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 20:23:42 2026 +0200"
      },
      "committer": {
        "name": "remm",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 20:23:42 2026 +0200"
      },
      "message": "Merge branch \u0027main\u0027 of git@github.com:apache/tomcat.git into main"
    },
    {
      "commit": "c5e83991e0ac7fdc77710ef22ce59b3ed859c166",
      "tree": "35b87deae846b7159c20569c5f684cfff41002dc",
      "parents": [
        "946ba68fefaf241f84cbfe0c5361bf99b1a1cabc"
      ],
      "author": {
        "name": "remm",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 19:23:51 2026 +0200"
      },
      "committer": {
        "name": "remm",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 19:23:51 2026 +0200"
      },
      "message": "Refactor certificate loading\n\nMove the certificate and private key loading out of OpenSSLContext into\na new reusable CertificateLoader utility class, to allow reuse (the\nQUIC-based HTTP/3 endpoints will need the same logic). PEM (with an\nENGINE fallback for unparseable keys), PKCS#12 and Java key store\nsources are supported; PKCS#12 is parsed with the JDK so a wrong\npassword is detected via the MAC check. The native objects of a loaded\nentry (leaf, key, chain) are owned by the configuration snapshot which\nloads them, and released with freeCertEntry() when it is discarded.\n\nThe Java X.509 chain view used by JMX and the certificate expiry checks\nmoves as well, and so does the TLS 1.3 pre-shared key find-session\ncallback: it is now installed on the SSL_CTX and resolves the per-host\nselector per connection, since a single listener serves all hosts.\n\nAlso:\n\n- Detect OpenSSL 4.x (OpenSSLStatus.isOpenSSL4()) which provides the\n  QUIC API, and add the d2i_AutoPrivateKey binding which is used to\n  convert Java private keys to native ones.\n- Complete and align the panama string bundle: add the FIPS provider\n  keys which OpenSSLLibrary and AprLifecycleListener already reference,\n  rename opensslconf.unknownCommandType to opensslconf.typeUnknown\n  which is the key OpenSSLContext uses, and drop the unused panama\n  engine.noSession key.\n"
    },
    {
      "commit": "9f023a755b788ba72e90ffff7df4334c51457b86",
      "tree": "402855fb322aafb2ac1e75bf3b54ad69640b3738",
      "parents": [
        "2945a01d1851c293b95e650203e1ebfef49c7fcd"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 15:36:41 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 15:36:41 2026 +0200"
      },
      "message": "Support AES-128-CBC and AES-192-CBC encrypted traditional PEM keys\n\nThe switch on the DEK-Info algorithm of a traditional PEM encrypted\nprivate key only handled DES-CBC, DES-EDE3-CBC and AES-256-CBC. Any\nother algorithm, including the AES-128-CBC and AES-192-CBC that OpenSSL\nreadily produces with openssl rsa -aes128 and -aes192, fell into a\ndefault branch that used the OpenSSL algorithm name directly as the JCE\ntransformation name. That always failed with a message like Cannot find\nany provider supporting AES-128-CBC, which does not say that the\nalgorithm is simply unsupported.\n\nAdd the two missing algorithms, using the same key derivation and the\nsame warning about the OpenSSL password based key derivation that the\nAES-256-CBC case uses, and report any other algorithm as unsupported\nwith the name of the PEM file and the algorithm instead of attempting to\nuse the OpenSSL name as a JCE transformation.\n\nTests: new fixtures key-encrypted-pkcs1-aes128.pem and\nkey-encrypted-pkcs1-aes192.pem, generated with OpenSSL from the existing\nkey-pkcs1.pem, cover the new algorithms. A new test checks that an\nalgorithm outside the supported set is reported as a\nNoSuchAlgorithmException that names the algorithm.\n"
    },
    {
      "commit": "2945a01d1851c293b95e650203e1ebfef49c7fcd",
      "tree": "165f5bbf44711b408f12d2ab34ccede759dcf7a3",
      "parents": [
        "dcf15069ef6e9e2009016d3d6e341e7d533ac42a"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 15:26:45 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 15:26:45 2026 +0200"
      },
      "message": "Report malformed structure in PEM files as an error\n\nThe scanner for the parts of a PEM file silently accepted files with a\nmalformed structure. A part that was never closed by an end boundary,\neither because the file ended or because the next start boundary\nappeared, was dropped without any error and the part was simply missing\nfrom the result. An end boundary whose type did not match the type of\nthe start boundary that opened the part was accepted. An end boundary\nthat did not follow a start boundary put a null part into the list of\nparts, leading to a null pointer exception later on. Boundary lines and\nDEK-Info lines were parsed without any checks, leading to an index out\nof bounds exception for truncated boundary lines and to a silently\nignored or incorrectly parsed DEK-Info line. For example, a DEK-Info\nline with white space after the comma was parsed with the white space\nkept as part of the initialisation vector, which only became visible as\na failure to decrypt the key.\n\nReject all of these with an IOException that describes the problem. This\nlets the connector fail at startup with a usable configuration error\ninstead of silently loading an incomplete file or failing later with an\nunrelated exception. White space at the end of a boundary line remains\ntolerated, since RFC 7468 allows it there.\n\nTests: 7 new tests in TestPEMFile for the malformed structures listed\nabove and for white space at the end of the boundary lines. All 23 PEM\nfiles in the existing test resources parse exactly as before.\n"
    },
    {
      "commit": "dcf15069ef6e9e2009016d3d6e341e7d533ac42a",
      "tree": "bf49daac95b9103f24f64570aebbc7000d8e3943",
      "parents": [
        "2f5925a16cdec0d0e70ce4adf0e2fefbeb2225f4"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 15:07:26 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 15:18:38 2026 +0200"
      },
      "message": "Fix the PEM output of PEMFile.toPEM()\n\nThe encoded text was written with the final line of the Base64 data joined\nonto the end boundary and without a line separator after the end boundary.\nThe result was not valid PEM: OpenSSL was unable to decode it\n(\"Could not find or decode certificate\") and Tomcat\u0027s own PEM parser did not\nsee any certificate in it, since the end boundary never started a line.\n\nWrite each boundary on a line of its own, terminate the encoded text with a\nline separator and encode with the line length of 64 characters that RFC\n7468 requires. The Base64 line separator is now the same as the one used for\nthe boundaries, so the output no longer mixes line separators when the JVM\nuses a line feed. The output of the method now has the same format as the one\nproduced by keytool -exportcert -rfc for the same certificate, apart from the\nline separator, which on a JVM using a line feed will be that separator\nrather than the carriage return line feed used by keytool.\n\nThese values are used for the mod_rewrite compatible SSL_SERVER_CERT,\nSSL_CLIENT_CERT and CERT_CHAIN variables, which are expected to contain a\nfull PEM encoded certificate as Apache HTTP Server\u0027s mod_ssl provides them.\n\nTests: testToPem encodes the certificate in the new certificate.pem test\nfile and checks the resulting structure as well as that the result can be\nread back as PEM. It previously failed because the end boundary was not on a\nline of its own.\n"
    },
    {
      "commit": "2f5925a16cdec0d0e70ce4adf0e2fefbeb2225f4",
      "tree": "e594203dcdeef855686122cac57acdc2fbf5df4b",
      "parents": [
        "7fe1211e1f3921b57384cf573cbc7f5c67dd1a5f"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 14:46:02 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 14:46:02 2026 +0200"
      },
      "message": "Report a configuration error for PEM certificates with a FIPS key manager\n\nWhen the KeyManagerFactory is provided by a FIPS module, Tomcat passes the\nconfigured key store to the factory directly and skips the manipulation of\nkey material that FIPS does not allow. PEM format certificates require that\nmanipulation: the key and certificate chain are read from the PEM files and\nimported into an in memory key store.\n\nPreviously, the FIPS code path used the configured key store without\nchecking it. For a PEM based configuration there is no key store, so the\nfactory was initialised with a null key store. Depending on the\nimplementation, that either produced a key manager with no keys, allowing\nthe connector to start but causing every TLS handshake to fail, or an\nunrelated error during start up.\n\nNow report the configuration error when no key store is available. The\nexisting message is used when no certificate has been configured at all, as\nfor a non-FIPS configuration. Otherwise, explain that PEM format\ncertificate files require a key store when a FIPS key manager factory is in\nuse.\n\nTests: testFipsWithPemConfiguration and testFipsWithoutConfiguration cover\nboth cases. Both previously failed because no error was reported.\n"
    },
    {
      "commit": "946ba68fefaf241f84cbfe0c5361bf99b1a1cabc",
      "tree": "94670a2548871919516e6f224c72b74a2f8d2979",
      "parents": [
        "9c3dd645817082bf8ed60f49ea59963d6ce2e482"
      ],
      "author": {
        "name": "Christopher Schultz",
        "email": "chris@christopherschultz.net",
        "time": "Thu Oct 15 13:37:41 2026 +0100"
      },
      "committer": {
        "name": "Christopher Schultz",
        "email": "chris@christopherschultz.net",
        "time": "Thu Oct 15 13:37:41 2026 +0100"
      },
      "message": "Align embedded distribution ZIP with other archive formats\n\nPut all files into a directory matching the release version number\n"
    },
    {
      "commit": "7fe1211e1f3921b57384cf573cbc7f5c67dd1a5f",
      "tree": "76cebd44715b8970f54ddbeaec2b4cdd9256c02e",
      "parents": [
        "5d50161f0f6b46166c9d06d4d9ca1e06dec04ae6"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 14:35:06 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 14:35:06 2026 +0200"
      },
      "message": "Fix TLS 1.3 cipher suite configuration\n\nSSLHostConfig.setCipherSuites() converted each configured name from JSSE\nformat to OpenSSL format and then discarded the result, appending the\noriginal value instead. Since the value is always stored, and later passed\nto OpenSSL, in OpenSSL format, append the converted name. This matches\nsetCiphers(), which already appended the converted name. There is no\nchange in behaviour for current configurations because the JSSE and\nOpenSSL names for TLS 1.3 cipher suites are currently the same.\n\nAlso avoid a NullPointerException when a null value is provided. That\nmethod dereferenced the argument without checking it, unlike setCiphers(),\nwhich stores a null value and resets the values derived from the previous\nconfiguration. Do the same here.\n\nTests: testCiphersuite04 covers the JSSE format (comma separated) list and\nthe conversion to the OpenSSL format list. testCiphersuite05 covers the\nnull value, which previously failed with a NullPointerException.\n"
    },
    {
      "commit": "5d50161f0f6b46166c9d06d4d9ca1e06dec04ae6",
      "tree": "87acd53283b1ded8451d514593bdc6992bcb3a58",
      "parents": [
        "6b1189e20927d3c50190f5aee680b7944494ed79"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 13:58:52 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 13:58:52 2026 +0200"
      },
      "message": "Fix broken SNI fallback for ClientHello values larger than the parse limit\n\nWhen a ClientHello could not be buffered within sniParseLimit,\nprocessSNI() selected the default SSLHostConfig but then still called\nthe TLSClientHelloExtractor getters, which throw IllegalStateException\nfor an incomplete extraction. The documented fallback therefore never\nhappened: the exception escaped to the socket processor\u0027s error\nhandler, logging an error and aborting the connection. Pass empty\nlists for all extractor-derived values (requested ciphers, application\nprotocols, protocol versions, cipher names, supported groups and\nsignature schemes) through local variables on the UNDERFLOW path\ninstead of consulting the extractor, which also prevents NPEs on the\nunguarded isEmpty()/contains() usages in createSSLEngine(). Add a test\nthat sends an oversized ClientHello record and asserts the default-host\nfallback is logged without an IllegalStateException.\n"
    },
    {
      "commit": "6b1189e20927d3c50190f5aee680b7944494ed79",
      "tree": "3878400f7a8b906de5acbe29d271e6480edc4c9d",
      "parents": [
        "cb7981933a95a4da8661e882eb9a98061952412b"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 13:19:04 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 13:19:04 2026 +0200"
      },
      "message": "Fix ArrayIndexOutOfBoundsException in IPv6Utils.canonize()\n\nInputs that pass the mayBeIPv6Address() heuristic but contain more than\n8 colon-separated groups (e.g. \"1:2:3:4:5:6:7:8:9\" or forms with more\nthan two consecutive colons), or groups with more than 4 significant\ncharacters (e.g. \"12345::\"), indexed the fixed-size group store out of\nbounds and threw an undocumented ArrayIndexOutOfBoundsException. With\nthe non-default access log attribute ipv6Canonical\u003d\"true\", such values\n(e.g. from a client-supplied Host header via the %v element) caused\nloss of the access log record plus a warning per request. canonize()\nnow returns such malformed inputs unchanged, consistent with how\nnon-IPv6 input is already passed through. Add tests for the\npass-through behaviour and for the valid 8-colon forms\n(\"1:2:3:4:5:6:7::\") whose canonicalization must be preserved.\n"
    },
    {
      "commit": "9c3dd645817082bf8ed60f49ea59963d6ce2e482",
      "tree": "f0741adaaf1379b2ed3630977effb11dce801a91",
      "parents": [
        "decfd3e92d34dcc497a43e2ae85e763d4268f8e8"
      ],
      "author": {
        "name": "Christopher Schultz",
        "email": "chris@christopherschultz.net",
        "time": "Thu Oct 15 12:02:15 2026 +0100"
      },
      "committer": {
        "name": "Christopher Schultz",
        "email": "chris@christopherschultz.net",
        "time": "Thu Oct 15 12:02:15 2026 +0100"
      },
      "message": "Bump spotbugs version\n"
    },
    {
      "commit": "cb7981933a95a4da8661e882eb9a98061952412b",
      "tree": "dcc70b21b6f36a49d0e72faf213ff6416a5cb1af",
      "parents": [
        "77dabda0dd9dde6bcf462e850d753170f12d80f7"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 12:36:30 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 12:36:30 2026 +0200"
      },
      "message": "Shut down the utility executor created by an endpoint\n\nWhen no utility executor was set, AbstractEndpoint.getUtilityExecutor()\ncreated one lazily. The executor was never shut down when the endpoint\nwas destroyed, so the non-daemon core thread of the scheduled thread\npool it created kept running and prevented JVM shutdown, which is\nreachable for embedded users that do not provide the shared executor a\nfull server installs from StandardServer.\n\nTrack an executor created by the endpoint and shut it down in\ndestroy(). Externally provided executors remain owned by the caller and\nare left running. The field is made volatile so the creation is safely\npublished. Concurrent lazy creation remains an unsynchronised\ncheck-then-act: the executor is only requested from the protocol\nlifecycle start and from the scheduled async timeout task, which are\nserialised per endpoint, the same tolerated pattern already used for\nthe connection latch in this class, and a duplicated pool in the\nhypothetical concurrent case would at worst match the previous\nbehaviour since callers schedule tasks on the instance they received.\nThe executor reference is deliberately not cleared on destroy so a\nlater call returns the shut-down instance rather than creating a new\npool that would never be destroyed.\n"
    },
    {
      "commit": "77dabda0dd9dde6bcf462e850d753170f12d80f7",
      "tree": "6634736f2357187dbd1df74335131a882b29e3ac",
      "parents": [
        "6a3a77986279ecf91520c6dd01e537de4532da2b"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 12:22:57 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 12:22:57 2026 +0200"
      },
      "message": "Document and clean up the descriptor load result\n\nModelerSource.loadDescriptors() is documented to return a list of\nobject names, but both sources provided with Tomcat registered the\ndescriptors they loaded with the given registry and never recorded any\nobject names, so the private fields backing the return value were dead\nand the returned list was always empty. No object name identifying a\nloaded descriptor exists in the modeler, so populating the list would\nhave invented a semantic that no code produces.\n\nRemove the dead fields and return the immutable empty list, following\nthe precedent already set by the no-descriptor registry, and document\nin ModelerSource and Registry.load() that the provided sources\nregister their descriptors with the registry and do not return object\nnames. Callers must not assume the returned list is mutable.\n"
    },
    {
      "commit": "6a3a77986279ecf91520c6dd01e537de4532da2b",
      "tree": "9f9361d43db77619dfbc82c328aff0a371e3572d",
      "parents": [
        "f1840664c017de9ec6b35cd01e2c4f8d462f43ca"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 12:17:03 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 12:17:03 2026 +0200"
      },
      "message": "Add the read-descriptors error message to the modules string bundle\n\nMbeansDescriptorsIntrospectionSource declares its own StringManager\nbound to the org.apache.tomcat.util.modeler.modules resource bundle but\nlogs with the key modules.readDescriptorsError, which existed only in\nthe org.apache.tomcat.util.modeler bundle used by the sibling digester\nsource through the inherited manager. StringManager has no fallback to\na parent package bundle and its single argument getString() returns\nnull for a key that is missing from its bundle, so a failed descriptor\nload logged an error entry without a message.\n\nAdd the message to the modules bundle, and add a test asserting that\nthe keys bound to that bundle resolve from the default locale as well\nas from the translated bundles, which fall back to the base bundle for\nkeys that are not yet translated.\n"
    },
    {
      "commit": "f1840664c017de9ec6b35cd01e2c4f8d462f43ca",
      "tree": "7ac4705850e9fa2297042ed46c8d8fd0170e0f0a",
      "parents": [
        "eb230f9c13ca9f1f6626c66e5a0e7e6efedeee3b"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 11:55:30 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 11:55:30 2026 +0200"
      },
      "message": "Quote ObjectName values containing a quote or a newline\n\nThe JMX parser rejects unquoted property values containing a double\nquote or a line feed, so such values must be passed through\nObjectName.quote() before being concatenated into an ObjectName\nstring. Util.objectNameValueNeedsQuote() listed only comma, equals,\ncolon and the wildcards, so servlet, filter and upgrade protocol\nnames containing a double quote bypassed the quoting and the\nsubsequent ObjectName construction failed. The call sites catch that\nfailure and log a warning, silently leaving the component without a\nJMX registration.\n\nAdd the double quote and the line feed to the character check. A\nsweep of the value position of a key property confirms these are the\nonly characters, besides the ones already listed, that the parser\nrejects in an unquoted value.\n"
    },
    {
      "commit": "eb230f9c13ca9f1f6626c66e5a0e7e6efedeee3b",
      "tree": "b2e355963fe87a3e02a0e08f65d20f19b66a53b7",
      "parents": [
        "7873dd616d0d8faae3f7727b59dc5aa6566a0618"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 11:45:40 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 11:45:40 2026 +0200"
      },
      "message": "Convert numeric and character values in the JMX proxy conversion helper\n\nRegistry.convertValue() only mapped String, ObjectName, int/Integer,\nlong/Long and boolean/Boolean type names. Every other type name fell\nthrough and the raw String was returned. Its sole consumer, the JMX\nproxy servlet of the Manager application, passes the returned value\ndirectly to MBeanServer.setAttribute() and invoke(), and the modeler\nMBeans apply no conversion of their own, so setting an attribute or\ninvoking an operation with a double, float, short, byte or char\nparameter on any modeler MBean always failed with an argument type\nmismatch.\n\nConvert the missing floating point and narrow integral types using the\nwrapper valueOf methods, giving the same decimal range and format\nchecking the existing integral branches already provide, and convert\nthe first character of a non-empty string for the character types. A\nnull or empty string is left unconverted rather than introducing a new\nexception path.\n"
    },
    {
      "commit": "7873dd616d0d8faae3f7727b59dc5aa6566a0618",
      "tree": "125b204f46182deb1aa73775c94c026d5f6124df",
      "parents": [
        "326ff6da5595a4132e060b5c47d60c922f417355"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 11:40:43 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 11:40:43 2026 +0200"
      },
      "message": "Report missing metadata when registering a component\n\nRegistry.findManagedBean() is documented to return null when no\nmetadata can be found for the requested type: the introspection\nfallback logs registry.noTypeMetadata and returns null, and the no\ndescriptor registry always returns null. Registry.registerComponent()\ndereferenced that result immediately, so the documented failure\nsurfaced as a NullPointerException logged under the generic\nregistry.registerError message instead of the descriptive metadata\nmessage.\n\nCheck the result and throw an IllegalStateException carrying the\nregistry.noTypeMetadata message, still inside the existing try block\nso the error is logged with the object name before it is rethrown.\n"
    },
    {
      "commit": "326ff6da5595a4132e060b5c47d60c922f417355",
      "tree": "f536d4e60af25d9521c965b6969fe294ef5b6272",
      "parents": [
        "fab78d508dbbb43ada170179a3f46c908adc1549"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 11:33:42 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 11:33:42 2026 +0200"
      },
      "message": "Close the descriptor stream in the digester modeler source\n\nRegistry.load() opens an input stream for URL and File sources and\nhands it to the modeler source without retaining any reference, so\nthe stream had to be closed by the source that consumed it.\nMbeansDescriptorsDigesterSource.execute() previously cast the source\nto InputStream and parsed it without ever closing it, relying on the\nunderlying SAX parser to release the stream. That left the release of\nthe resource to unspecified parser behaviour.\n\nValidate the source with an instanceof check instead of a blind cast,\nthrowing IllegalArgumentException for anything other than an\nInputStream, and parse inside a try-with-resources statement so the\nstream is closed when the parse completes, when it fails and when an\nunchecked error occurs.\n\nAdd unit tests that pass a close-tracking input stream through\nRegistry.load() and assert the stream is closed after both a\nsuccessful parse and a parse error, and that a non-InputStream source\nis rejected with IllegalArgumentException.\n"
    },
    {
      "commit": "fab78d508dbbb43ada170179a3f46c908adc1549",
      "tree": "55564983bf69bd28c4bd818088a55d9bc8e68c34",
      "parents": [
        "ef9076402d190cef7294cc9120baeba8b5847776"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 10:55:01 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 10:55:04 2026 +0200"
      },
      "message": "Synchronize the registry metadata maps\n\nThe descriptors, descriptorsByClass and searchedPaths maps were plain\nHashMaps mutated by addManagedBean() and loadDescriptors() and read\nby findManagedBean(), registerComponent() and findDescriptor() with no\nsynchronization, while registerComponent() is invoked concurrently\nduring parallel web application deployment and from worker threads on\nfirst use of an upgrade protocol. Concurrent access could lose\nupdates or transiently corrupt the map state, and stop() additionally\nreassigned the three non-volatile fields while readers were active.\n\nMake the three maps final ConcurrentHashMaps and have stop() clear\nthem rather than reassign them. Record the searched package\natomically with putIfAbsent so concurrent first loads of the same\npackage can not both parse the descriptor file, and skip the\nname-keyed entry when a ManagedBean has no name (permitted by the\nDTD) since ConcurrentHashMap does not allow null keys, preserving the\nprevious behavior for such descriptors.\n"
    },
    {
      "commit": "ef9076402d190cef7294cc9120baeba8b5847776",
      "tree": "58dcb60eb9132cd9a467e9f862377eab8c5afef3",
      "parents": [
        "a80eb6743ebe802122ac97a32181a173f54bdcd5"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 10:49:05 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 10:49:05 2026 +0200"
      },
      "message": "Invalidate the cached MBeanInfo when attributes or operations are added\n\nManagedBean caches the MBeanInfo it builds and every other mutator\n(setName, setClassName, setDescription, setType, addNotification)\nnulls the cache under the mBeanInfoLock write lock, but addAttribute\nand addOperation did neither. Adding an attribute or operation after\ngetMBeanInfo() had been called would therefore keep serving a stale\nMBeanInfo, and writing the descriptor maps without the lock was also\ninconsistent with getMBeanInfo() reading them under the write lock.\n\nMake both methods mirror addNotification: hold the write lock, update\nthe map, and clear the cached info. Add a unit test that verifies the\nMBeanInfo is rebuilt and contains the newly added descriptor.\n"
    },
    {
      "commit": "a80eb6743ebe802122ac97a32181a173f54bdcd5",
      "tree": "3166f28fed080827ebe4a8705388f7c19205c2af",
      "parents": [
        "3c5fdd5b6941ac01c793c06ab0e4222875a4bddc"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 10:44:16 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 10:44:16 2026 +0200"
      },
      "message": "Throw InstanceNotFoundException from the no-JMX MBean server\n\nWhen JMX support is disabled with --no-jmx, Registry.getMBeanServer()\nreturns the NoJmxMBeanServer whose methods returned null or silently\ndid nothing where the MBeanServer contract requires an exception or a\nnon-null value. Downstream code that trusted the contract then hit\nnull pointer exceptions, for example Registry.getMethodInfo()\ndereferencing the null MBeanInfo returned by getMBeanInfo (reached\nthrough the Manager application JMX proxy) and the boolean unboxing\nof the null result of invoke in ManagerServlet.isDeployed() and\ntryAddServiced().\n\nSince no MBean can ever be registered with this server, the truthful\ncontract-compliant answer for any operation referencing an object\nname is InstanceNotFoundException, and the count of registered MBeans\nis zero. Update all such methods accordingly and return 0 from\ngetMBeanCount. Methods without an InstanceNotFoundException throws\nclause that are unreachable through the overridden no-op Registry\nmethods keep their previous behavior.\n\nAdd a unit test covering the previously failing contract paths.\n"
    },
    {
      "commit": "3c5fdd5b6941ac01c793c06ab0e4222875a4bddc",
      "tree": "b1306df0ed37d5bd3e280ae1744a810bb9d3bd3d",
      "parents": [
        "26cc177236f57d7f4e60ad56c10280d53fc1ed0a"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 10:30:56 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 10:30:56 2026 +0200"
      },
      "message": "Use unique increasing sequence numbers for MBean notifications\n\nBaseModelMBean emitted every JMX notification with the hard-coded\nsequence number 1, which prevents listeners that use the sequence\nnumber for ordering or to detect missed or duplicated notifications\nfrom doing so, contrary to the JMX requirements for notification\nsources.\n\nAdd a per-MBean sequence number counter (AtomicLong since\nnotifications may be emitted from several admin threads) and use it\nat both notification construction sites so attribute change\nnotifications and generic notifications share one increasing\nsequence. Notifications built by callers and passed to the\nsendNotification/sendAttributeChangeNotification overloads keep the\nsequence number assigned by their constructing sender as per the JMX\ncontract.\n\nAdd a unit test asserting the exact sequence numbers of two attribute\nchange notifications and a following generic notification.\n"
    },
    {
      "commit": "26cc177236f57d7f4e60ad56c10280d53fc1ed0a",
      "tree": "822a0ea4d3ac06dcd929b0ed4850dbe963dd8664",
      "parents": [
        "e00a3d64af895fd6b66471631edafe19d4f2f74d"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 10:18:58 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 10:18:58 2026 +0200"
      },
      "message": "Report the actual old value in attribute change notifications\n\nBaseModelMBean.setAttribute() sent every AttributeChangeNotification\nwith a null old value since the code fetching the previous value had\nbeen commented out (a leftover from the removed attribute cache),\nwhich breaks the AttributeChangeNotification contract for listeners\nthat rely on the old value.\n\nRetrieve the previous value via getAttribute() before invoking the\nsetter. Attributes without a getter, or whose getter fails, keep the\nunknown old value null so write-only or failing-getter sets behave\nexactly as before. As a consequence, a set to null is now reported\n(old and new values both null is still treated as no change).\n\nAdd a unit test covering the old and new values of the notification\nemitted by setAttribute().\n"
    },
    {
      "commit": "e00a3d64af895fd6b66471631edafe19d4f2f74d",
      "tree": "41d782634600c9d0c52813cb363812fdf331eae3",
      "parents": [
        "d607e2e70ee145cc44a2f49ec5e1339141f75401"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 10:12:47 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 10:12:47 2026 +0200"
      },
      "message": "Fix name-scoped removal of attribute change notification listeners\n\nBaseModelMBean.removeAttributeChangeNotificationListener() previously\nremoved every attribute change registration for the given listener\ninstead of only the registration for the requested attribute name\n(the long-standing FIXME in the method).\n\nAdd a removal method to BaseNotificationBroadcaster that is scoped to\nan attribute name. Since addNotificationListener() coalesces\nregistrations with the same listener and handback into a single entry\nwith a multi-name BaseAttributeFilter, removal drops the name from the\nfilter and only removes the entry when no explicit name remains, so a\nsingle-name filter is never silently turned into an accept-all filter.\nRegistrations that accept all attribute names (created with a null\nattribute name) can only be removed by passing a null name, mirroring\nthe registration semantics. Entries registered with other filters are\nleft untouched.\n\nAlso add a unit test covering the coalesced, per-handback, accept-all\nand non-attribute-filter cases.\n"
    },
    {
      "commit": "d607e2e70ee145cc44a2f49ec5e1339141f75401",
      "tree": "bd66b2a7248e468a4b986093261bffcad0de6cf6",
      "parents": [
        "15d1692edf7f76a8cb18107fe076310974c266fd"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 09:37:11 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 09:37:11 2026 +0200"
      },
      "message": "Parse exponent-only numbers in the JSON parser\n\nNumbers with an exponent and no fraction part, like 1e5, were rejected.\nThe NUMBER_DECIMAL token required a fraction, with the exponent as an\noptional part of it, so a number such as 1e5 matched no number token and\nthe parser failed, while Double.parseDouble and BigDecimal both accept\nit. Such numbers are also valid per RFC 8259.\n\nMake the NUMBER_DECIMAL token accept either a fraction with an optional\nexponent, or an exponent on its own, after the integer part, and\nregenerate the token manager.\n\nNote that in the lenient dialect, such numbers used as bare object keys\nare formatted using the string representation of the parsed number, so\n1e5 becomes the key 1E+5. Previously, such keys were rejected.\n"
    },
    {
      "commit": "15d1692edf7f76a8cb18107fe076310974c266fd",
      "tree": "c65bc2fc5dab7f8685d4847bcaeddc761e29faf8",
      "parents": [
        "13a8b44f8309a1d76429477e17819ba0b7df4a61"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 09:16:29 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 09:16:29 2026 +0200"
      },
      "message": "Parse out-of-range native JSON numbers as BigInteger\n\nWith native numbers enabled, integers were converted with Long.valueOf\nwhile the grammar admits integers of arbitrary digit count, so values\noutside the long range caused an unchecked NumberFormatException rather\nthan being parsed.\n\nSuch integers now fall back to BigInteger, matching the behaviour of the\ndefault non-native mode which already uses BigInteger. Values within the\nlong range continue to produce Long instances. The change is made in the\ngrammar (JSONParser.jjt) and mirrored in the checked-in generated\nparser, with regression tests for both sides of the boundary.\n"
    },
    {
      "commit": "13a8b44f8309a1d76429477e17819ba0b7df4a61",
      "tree": "7983f838a208806932689b658dad57b1a2a52c8c",
      "parents": [
        "decfd3e92d34dcc497a43e2ae85e763d4268f8e8"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 09:06:02 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Thu Oct 15 09:06:02 2026 +0200"
      },
      "message": "Add a maximum nesting depth to the JSON parser\n\nThe parser recursed once per nesting level with no limit, so deeply\nnested input caused a StackOverflowError. That Error also escaped the\nconsumers of the parser, which typically only catch Exception.\n\nThe parser now counts nesting at the entry point of the anything()\nproduction and throws a ParseException once the depth exceeds 1000.\nRFC 8259 allows implementations to set a limit on the maximum depth of\nnesting. A depth of 1000 is far beyond what realistic JSON documents\nuse and matches the limit used by other mainstream JSON parsers. The\ncount is reset by the public entry points parseObject(), parseArray()\nand parse() so that a parser remains usable after a failed parse.\n\nThe change is made in the grammar (JSONParser.jjt) and mirrored in the\nchecked-in generated parser. Regression tests cover acceptance at the\nlimit, rejection above it, and reuse of a parser after a depth error.\n"
    },
    {
      "commit": "decfd3e92d34dcc497a43e2ae85e763d4268f8e8",
      "tree": "d5d7861f9c5ad8eff45b42341dcf27c013033c7c",
      "parents": [
        "8a80cb4d5203f6ab9a72626f116453d662438e6a"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 21:28:28 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 21:28:28 2026 +0200"
      },
      "message": "Treat an empty port when parsing a host value as an absent port\n\nRFC 3986 (section 3.2.3) allows a \u0027:\u0027 separator that is not followed\nby any digits. HttpParser.validatePort() already accepted such a value\nbut AbstractProcessor parsed zero digits for the port, associating a\nport of zero with the request.\n\nAbstractProcessor now treats an empty port as equivalent to an absent\nport, keeping the port already associated with the request rather than\noverwriting it with zero.\n\nDocument the empty port behaviour of validatePort() for its callers and\nadd tests for host values with an empty port.\n"
    },
    {
      "commit": "8a80cb4d5203f6ab9a72626f116453d662438e6a",
      "tree": "5bdfdc685557107cf7028875e08cd41f1e7b2fdd",
      "parents": [
        "06d7c514ba9ffa417efbe163ca6d57e7b3956f6c"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 20:34:50 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 21:04:05 2026 +0200"
      },
      "message": "Ignore parameters with no name when parsing a MediaType\n\nA dangling \u0027;\u0027 at the end of a media type is invalid but tolerated by\nthe parser. Previously it resulted in a phantom parameter with an\nempty name and an empty value being added to the parsed MediaType,\ninflating getParameterCount() (e.g. \"text/html;\" reported one\nparameter). Trailing \";;\" had the same effect, inconsistent with the\nhandling of a \";;\" sequence in the middle of the value where the null\nattribute is skipped and no parameter is stored.\n\nreadToken() only returns the empty string at end of input, so an empty\nattribute can never carry a real name; attributes with an empty name\nare now ignored in the same way as null attributes.\n"
    },
    {
      "commit": "06d7c514ba9ffa417efbe163ca6d57e7b3956f6c",
      "tree": "96f65fe1a2dfff54278b93eb79a0549a3880ecc3",
      "parents": [
        "f301588994cf48bc1dcc32e137ca7ca21ca68ed4"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 20:01:42 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 21:04:05 2026 +0200"
      },
      "message": "Omit invalid parameter values when serializing a MediaType without charset\n\ntoString() skipped parameters with a null or empty value but\ntoStringNoCharset() did not, so a media type such as\n\"text/html;charset\u003dutf-8;foo\u003d\" re-serialised by Response with the\nliteral string \"foo\u003dnull\" (null values occur for unterminated quoted\nstrings and mid-string empty values) or with an empty \"foo\u003d\". The\ncorrupted value was then written to the Content-Type response header.\n\ntoStringNoCharset() now applies the same null/empty value guard as\ntoString(). Callers (coyote Response.setContentType and\nMediaTypeCache used by catalina connector Response) only use the\nno-charset serialisation when a charset is present, so this simply\nremoves the corruption; programmatic access to the parameters via\ngetParameterValue() is unchanged.\n"
    },
    {
      "commit": "f301588994cf48bc1dcc32e137ca7ca21ca68ed4",
      "tree": "630553d4929af0f9f3a8f308637b7be4bab3309a",
      "parents": [
        "d5f1d40f02bb9a310849116eca4cccc2ee51cf00"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 19:15:09 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 19:15:09 2026 +0200"
      },
      "message": "Replace obs-fold with a single SP when parsing headers\n\nThe header parser\u0027s line folding (obs-fold) handling peeked the first\nbyte of the continuation line and copied that raw byte into the\ncompacted header value. Since an obs-fold may start with HTAB, a TAB\ncould end up in the value, e.g. a header sent as \"X: abcd CRLF TAB\nefgh\" was stored as \"abcd\\tefgh\".\n\nRFC 9112 section 5.2 requires a server that receives an obs-fold to\neither reject the message or replace each obs-fold with one or more SP\noctets. The parser now always inserts a single space at the fold point\ninstead of copying the peeked byte. Whitespace beyond the first byte of\nthe continuation line was and is skipped by the value-start state, so\nthe stored value for the HTAB case changes from a TAB to a space while\nthe SP case is unchanged. This also removes a divergence between what\nTomcat sees and what a spec-compliant intermediary would forward for\nthe same message.\n\nThe existing test for folded headers (bz 48839) asserted the legacy\nbehaviour with a tab in the value and was updated to expect the single\nspace mandated by the specification. A new test pins the SP\ncontinuation case.\n"
    },
    {
      "commit": "d5f1d40f02bb9a310849116eca4cccc2ee51cf00",
      "tree": "f4c0acb961d42e434c6a6f5b177dc20d2887153a",
      "parents": [
        "6ded02c632106daf971f2159c493dd38fffe545d"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 18:59:19 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 18:59:19 2026 +0200"
      },
      "message": "Reject If headers containing an empty list in the WebDAV servlet\n\nRFC 4918 section 10.4.2 requires a WebDAV If header list to contain at\nleast one condition (List \u003d \"(\" 1*Condition \")\"). The parser in\nWebdavIfHeader previously accepted an empty list, such as \"If: ()\",\n\"( )\" or a list without any valid entry, and returned a non-null\nstructure for it. An empty IfList.match() vacuously returned true, so\nthe header passed the If precondition checks for any token and ETag,\nand hasValue() returned true so the strict mode rejection of invalid\nIf syntax in WebdavServlet.checkIfHeaders() was bypassed.\n\nparseIfList() now returns null when it did not read at least one valid\nentry, and parseUntagged() and parseTagged() propagate this so the\nwhole header value is treated as invalid. This routes processing into\nthe existing invalid syntax handling: with strictIfProcessing enabled,\nthe default, requests using such headers now fail the precondition\ncheck; with strictIfProcessing disabled the header is ignored, as for\nany other invalid If header syntax, and lock tokens in the raw header\nare still evaluated separately by isLocked(). Lenient error recovery\nfor entries that fail to parse is retained where other valid entries\nwere read.\n\nAdd TestWebdavIfHeader covering the parser behavior and new\nTestWebdavServlet cases covering the strict and non-strict servlet\nbehavior for \"If: ()\".\n"
    },
    {
      "commit": "6ded02c632106daf971f2159c493dd38fffe545d",
      "tree": "19756b0e72ef656bf6c5dc75fcb3c6bfbb6f31f4",
      "parents": [
        "c8924aa6bbbb25db4e14ca12b0b5eeda26c91f2f"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 17:41:22 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 17:41:22 2026 +0200"
      },
      "message": "Return a URI from ConfigurationSource.DEFAULT.getURI for invalid URI names\n\ngetURI() is documented to return URIs even for locations where no\nresource exists. For a relative name of a file that does not exist, the\nDEFAULT implementation ended by resolving the name against the user\ndirectory URI. URI.resolve() parses the reference strictly, so a name\ncontaining characters that are illegal in a URI but valid in a file\nname, such as a space, threw an unchecked IllegalArgumentException\ninstead, contradicting the contract and inconsistent with getResource(),\nwhich wraps the same resolve() call and reports FileNotFoundException.\n\nCatch the IllegalArgumentException in the missing file case and fall\nback to the file based URI, which is valid whether or not the file\nexists and percent-encodes the name correctly. Resolvable names keep\ntheir existing resolve() result unchanged, including scheme based\nnames such as classpath: and jar: that UriUtil does not classify as\nabsolute URIs and which resolve() returns intact.\n\nAdds TestConfigurationSource covering the missing file with a space,\nthe unchanged results for a missing URI-safe name, an existing file, a\nscheme based name and an absolute URI name.\n"
    },
    {
      "commit": "c8924aa6bbbb25db4e14ca12b0b5eeda26c91f2f",
      "tree": "ef88aa2756f79d4bff7a0f9aafb47dfb2321d663",
      "parents": [
        "581ea1b182ed0207e7283502712dd6db62dc491c"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 17:31:19 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 17:31:19 2026 +0200"
      },
      "message": "Decode service binding entry files as UTF-8\n\ngetProperty() decoded the binding file bytes with new String(byte[],int,\nint), which uses the JVM default character set. Service binding entries\nunder SERVICE_BINDING_ROOT are text files that follow the UTF-8\nconvention (Kubernetes secret projections encode stringData as UTF-8)\nand the resolved values flow into configuration such as SSLHostConfig\ncertificate key passwords, so on a JVM whose default character set is\nnot UTF-8 every non-ASCII character was replaced with a question mark,\nproducing a silently wrong credential.\n\nDecode the bytes as UTF-8 explicitly. Since the service binding\nspecification fixes no encoding, the class Javadoc now states that\nentry files are decoded as UTF-8 and that invalid bytes become U+FFFD.\n\nAdds TestServiceBindingPropertySource covering the UTF-8 decoding of\nmulti-byte characters (with and without the chomp: prefix), the full\nchomp matrix and the null results for unknown bindings and malformed\nkeys. The environment variable cannot be set from the tests, so a\npackage-private constructor pins the binding root for the test class;\nthe public no-argument path is unchanged and still reads the\nenvironment on every resolution. Note that the tests pass on the\ndefault-UTF-8 test JVM regardless of the fix; they fail against the\nprevious code when the default character set is not UTF-8, and they\npin the contract either way.\n"
    },
    {
      "commit": "581ea1b182ed0207e7283502712dd6db62dc491c",
      "tree": "fd1482109aadb7ca776d1e5a107a6c64a7fbe75c",
      "parents": [
        "6eb71c44c0bc24d33c135d67233a181b6822b186"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 17:14:02 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 17:14:02 2026 +0200"
      },
      "message": "Emit a null literal for missing CallMethodRule code-gen parameters\n\nA CallMethodRule collecting two or more parameters passes a parameter\nwhose nested element is absent to the target method as a null value.\nThat is documented behaviour, and the method call itself handles it.\n\nThe code generation block at the end of end() did not. A null parameter\nis not a String, so it was handed to Digester.toVariableName() to name a\nvariable for it. That method registers the object and then dereferences\nit, so the generated code path aborted the parse with a\nNullPointerException, wrapped in a SAXException, as soon as a\nmulti-parameter element omitted one of its parameter children, for\nexample an init-param without an init-value. This affects the opt-in\n-generateCode path used to pre-compile descriptor processing.\n\nEmit the null literal for a null parameter instead. The generated\nstatement then reflects the call that is actually made. All first-party\ntargets of these rules declare a single method of that name, so the\ngenerated source resolves to the same method the reflective invocation\nselected; a cast is not needed and would require canonicalising type\nnames for no first-party benefit.\n"
    },
    {
      "commit": "6eb71c44c0bc24d33c135d67233a181b6822b186",
      "tree": "c9cbecfddbdbe8578f6922fed2f920a624aaeb24",
      "parents": [
        "ced49783cb538461f75e66f3bb0eb8d758e5338d"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 16:58:30 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 16:58:30 2026 +0200"
      },
      "message": "Signal EmptyStackException for a negative ArrayStack peek index\n\nArrayStack.peek(int) computed [size() - n - 1] and threw\nEmptyStackException only when that index fell below zero, which covers\nindexes beyond the bottom of the stack. A negative index moved the\ncomputed position beyond the end of the backing list instead, so get()\nthrew an IndexOutOfBoundsException.\n\nThat exception escaped Digester.peek(int), which only catches\nEmptyStackException, even though the method is documented to return\nnull for any out of range index. One path where a negative index can be\npassed in is CallMethodRule with a negative targetOffset deeper than\nthe current stack depth, where digester.peek(getCount() + targetOffset)\nthen reported a wrapped IndexOutOfBoundsException rather than the\nintended \u0027Call target is null\u0027 parse error that names the offset and\nthe stack depth.\n\nGuard both ends of the range instead. For non-negative indexes the\ncondition is equivalent to the previous one, so no legitimate use\nchanges, and the outcome for extreme values no longer depends on\ninteger wrap-around.\n\nAdds TestArrayStack covering the top, the bottom, the empty stack and\nboth out of range directions including Integer.MIN_VALUE and\nInteger.MAX_VALUE, plus a TestDigester test for the peek(int) null\ncontract for negative indexes.\n"
    },
    {
      "commit": "ced49783cb538461f75e66f3bb0eb8d758e5338d",
      "tree": "da643e8354635315c1c626ac40b2ded967cf2c02",
      "parents": [
        "a6f4819e7deab1956b6611edb2c23e82132eb73d"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 16:52:23 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 16:52:23 2026 +0200"
      },
      "message": "Keep the Digester loggers usable after clear()\n\nclear() nulled log and saxLog. It runs at the end of every successful\nparse via endDocument(), on every failed parse via the reset added in\nthe previous commit, and via the public reset() method, so afterwards\nthe loggers stayed null until the next parse re-ran configure().\n\nThat left several public methods broken in exactly the empty state\ntheir Javadoc promises a null return or a plain call: peek(), peek(int)\nand pop() threw a NullPointerException inside their\ncatch(EmptyStackException) handler instead of returning null, peekParams\nand popParams behaved the same way, popParams(), pushParams() and\nregister() hit a NullPointerException unconditionally on the trace\nenablement check, and createSAXException(String, Exception) did so on\nits log.error path when no document locator has been set, for example\nafter a parse failure on a file that could not be opened.\n\nThe nulling also served no purpose. JULI loggers are cached by name by\nthe logging implementation, so retaining the two handles between parses\ncosts nothing, and configure() re-assigned equivalent handles on every\nsubsequent parse anyway. Nothing reads the fields to test for null and\nno Digester subclass in the tree overrides configure().\n\nAdds regression tests to TestDigester for the stack and parameter\nmethods after a successful parse and for createSAXException() in the\nno-locator state left by a failed parse; both fail with a\nNullPointerException without this change.\n"
    },
    {
      "commit": "a6f4819e7deab1956b6611edb2c23e82132eb73d",
      "tree": "42e4e233496c29145346f982d01dad550fdbdf90",
      "parents": [
        "fe6ce0b16c9c112a5f2fa58c7532fbbc5d6ac66d"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 16:44:23 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 16:44:23 2026 +0200"
      },
      "message": "Reset Digester state when a parse fails before end of document\n\nWhen getXMLReader().parse() aborted with a fatal SAX error, IOException\nor an exception from a rule, endDocument() and with it clear() never ran.\nThe Digester was left with a partially built match path, entries on the\nmatches stack, the parameter array pushed by CallMethodRule, objects on\nthe object stack and a stale root object. A subsequent parse on the same\ninstance then built its match patterns on top of the stale path, so\nrules stopped matching at the path they were registered for or matched\nat the wrong depth, and the leftover objects shifted the stack offsets\nthe rules rely on.\n\nThis is reachable in first-party code: ContextConfig.contextConfig()\nparses up to three context.xml files with one shared Digester and keeps\nusing it after processContextConfig() swallows a parse exception, and\nStoreLoader.load() re-parses with the same instance after an\nIOException.\n\nAll parse() overloads now restore a consistent state (clear() plus a\nnull root) before rethrowing. clear() also clears the matches stack and\nthe namespace prefix mappings, which leaked on every failed parse even\nvia reset(); they are empty by the time endDocument() calls clear() on\nthe success path, so this only removes residue. The code generation\nbuffer is deliberately left alone because ContextConfig writes the\ngenerated file after processContextConfig() even when the parse failed.\n\nAdds TestDigester covering the residual state after a failed parse and\nrule matching on the parse following a failure; both tests fail against\nthe previous code.\n"
    },
    {
      "commit": "fe6ce0b16c9c112a5f2fa58c7532fbbc5d6ac66d",
      "tree": "eb49807634dfef922761166d1a8fac62c5693321",
      "parents": [
        "ad2143b206f1f2ce514632dd2f3f2090f6e3d1a1"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 14:29:42 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 14:29:42 2026 +0200"
      },
      "message": "Validate the cookie max-age value at deployment time\n\nSessionConfig.setCookieMaxAge(String) stored the raw string and\ngetCookieMaxAge() parsed it lazily. The value is copied verbatim by\nContextConfig into the runtime SessionCookieConfig, where\nApplicationSessionCookieConfig.getMaxAge() parses it again on every\nsession cookie creation. Web.xml XSD validation is off by default, so an\ninvalid value such as \"abc\" passed parsing and then threw a raw\nNumberFormatException on every session creation and session id change\nfor the application, instead of failing the deployment.\n\nThe setter now validates the value with Integer.valueOf() before\nstoring it, consistently with setSessionTimeout() in the same class. The\ndigester wraps the resulting NumberFormatException into a SAX parse\nerror that reports the location in the descriptor and fails the\ndeployment cleanly. Valid values, including negative ones used to\ndisable cookie expiry, are stored unchanged.\n\nThe generic cookie attribute path (SessionConfig.setCookieAttribute, the\ndescriptor mirror of SessionCookieConfig.setAttribute) accepts arbitrary\nstrings and remains unvalidated, which is pre-existing and mirrors the\nprogrammatic API.\n\nAdds TestSessionConfig for the setter and parse level tests in\nTestWebXmlParser covering valid, invalid and empty max-age elements.\n"
    },
    {
      "commit": "ad2143b206f1f2ce514632dd2f3f2090f6e3d1a1",
      "tree": "0113acaef5dc0898525329185f824915e76ddd0d",
      "parents": [
        "5f6da06c35a9bdfb296e107d3cd689bc2d16b560"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 14:22:20 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 14:22:20 2026 +0200"
      },
      "message": "Return false rather than throw for SecurityRoleRef.equals(null)\n\nSecurityRoleRef.equals(Object) compared getClass() on the argument\nwithout a null check, so equals(null) dereferenced null and threw\nNullPointerException, violating the Object.equals contract which\nrequires false for a null argument.\n\nReplace the getClass() comparison with an instanceof pattern match,\nconsistent with LoginConfig in the same package, which handles both the\nnull argument and foreign types by returning false. There are no\nsubclasses of SecurityRoleRef in the tree, so behaviour is unchanged for\nall non-null inputs; hashCode is untouched and remains consistent with\nequals. No internal call site passes null (HashSet operations\nnull-guard the query key), so this is a latent robustness fix.\n\nAdds TestSecurityRoleRef covering the null argument, the field\ncomparison matrix and equality of instances with null fields.\n"
    },
    {
      "commit": "5f6da06c35a9bdfb296e107d3cd689bc2d16b560",
      "tree": "871bd26e7f76711580d09324523ac04314410ec9",
      "parents": [
        "251751fcbbb4853f3ee25235d78cfa11a1119823"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 14:13:53 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 14:13:53 2026 +0200"
      },
      "message": "Default filters to async unsupported unless set to true\n\nFilterDef.asyncSupportedBoolean defaulted to true and any value\nother than \"false\"/\"no\" was parsed as true. The Servlet specification\ndefaults filters to not supporting async processing.\n\nThe web.xml path masked the incorrect default because ContextConfig\nnormalises a missing async-supported element to \"false\", but filters\nregistered dynamically through ServletContext.addFilter() that never\ncalled Dynamic.setAsyncSupported(true) were treated as async supported,\nso ApplicationFilterChain applied them to ASYNC and ERROR dispatches\ncontrary to the specification.\n\nThe boolean field could also disagree with the stored string: values\nsuch as \"yes\" or \"1\" returned true from getAsyncSupportedBoolean() but\nfalse from the getAsyncSupported().equals(\"true\") checks used elsewhere,\nso the same filter was inconsistently treated as async supported and\nasync unsupported.\n\nasyncSupportedBoolean now defaults to false and the setter derives it\nwith \"true\".equalsIgnoreCase(), consistent with ServletDef and the\nBoolean parsing used by the other descriptor setters. The string value\nis stored unchanged so a storeconfig round-trip preserves the original\ndescriptor value.\n\nThis is a user-visible behaviour change: dynamically registered filters\nnow require an explicit setAsyncSupported(true) to receive async and\nerror dispatches. Annotation-defined filters and the shipped filter\ndefinitions are unaffected. Adds tests for the default and the full\nsetter matrix, plus a changelog entry.\n"
    },
    {
      "commit": "251751fcbbb4853f3ee25235d78cfa11a1119823",
      "tree": "b89f6b187f8ac86181b99fb3dc0b0cfcdcf8ec3a",
      "parents": [
        "12b87961e468f671b6372c2167e42f8727972e94"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 13:28:54 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 13:28:54 2026 +0200"
      },
      "message": "Store ContextService port-component refs outside generic properties\n\nThe port-component-ref mappings of a \u003cservice-ref\u003e (the Service\nEndpoint Interface to port-component-link associations) were stored in\nthe generic properties map inherited from ResourceBase, via\naddPortcomponent() -\u003e setProperty(). The same map also holds the\n\u003cmapped-name\u003e value, which MappedNameRule stores under the key\nmappedName for every resource that allows that element. Since\ngetServiceendpoints() simply returned listProperties(), any\n\u003cservice-ref\u003e that declared a \u003cmapped-name\u003e - which is legal per the\nServlet specification - was reported as having a bogus service endpoint\nnamed mappedName, whose port-component-link was the mapped-name\nvalue.\n\nThat bogus endpoint leaked to several consumers:\n\n  * NamingContextListener.addService() bound a\n    SERVICEENDPOINTINTERFACE/PORTCOMPONENTLINK pair for mappedName\n    into the application\u0027s JNDI ServiceRef.\n  * WebXml.toXml() serialized an extra\n    \u003cport-component-ref\u003e\u003cservice-endpoint-interface\u003emappedName\n    \u003c/service-endpoint-interface\u003e...\u003c/port-component-ref\u003e element into\n    the effective web.xml.\n  * ContextService.toString() printed mappedName as an endpoint, as did\n    the storeconfig output for a global ServiceRef.\n\nPort-component mappings now live in a dedicated map, kept separate from\nthe generic properties, and are included in equals()/hashCode().\nWebXml.toXml() reads the port link through getPortlink() rather than by\ntreating the endpoint name as a property key; the mapped-name element\ncontinues to be read from the mappedName property. As a side effect,\nendpoint names are no longer emitted as generic attributes when a global\n\u003cServiceRef\u003e is written by storeconfig.\n\nTests: TestWebXml now checks that a service-ref with a mapped-name\nreports only its real endpoint, retains the mapped-name as a property,\nand serializes a single correct port-component-ref in the effective\nweb.xml.\n"
    },
    {
      "commit": "12b87961e468f671b6372c2167e42f8727972e94",
      "tree": "55c6cf08573fb1e003fac40d3498fc980b1f0214",
      "parents": [
        "327ea63765e29dceb8b3dafd5c3ec445f1aa08eb"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 13:10:41 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 13:10:41 2026 +0200"
      },
      "message": "Reject a null URL in the TldResourcePath constructors\n\nTldResourcePath accepted a null URL without complaint and then failed\nlater with a NullPointerException in toExternalForm(), openStream(),\nequals() or hashCode() when the value was first used.\n\nOne caller made this reachable in normal operation. TldScanner\nresolved a TLD listed by ServletContext.getResourcePaths() with\nServletContext.getResource() and passed the result straight to the\nconstructor. The two calls are not atomic: a TLD removed from an\nexploded web application between them resolved to null, the resulting\nNullPointerException was thrown out of the scan and, since\nJasperInitializer.onStartup() only catches IOException and\nSAXException, it aborted context start-up with an opaque error.\n\nBoth constructors now throw an IllegalArgumentException with a\nreadable, localised message when the URL is null, mirroring the\nvalidation style used elsewhere in the utility packages. In addition,\nTldScanner.parseTld(String) now checks for the vanished resource and\nskips it with a warning, in the same way as scanJspConfig() handles a\ntaglib-location in web.xml that does not exist, so the race becomes a\nstartup warning rather than a failure.\n\nTests: TestTldResourcePath covers the constructor rejection and the\nmethods the invariant protects; TestTldScanner covers the scan-time\nwarn-and-skip of a resource that disappeared.\n"
    },
    {
      "commit": "327ea63765e29dceb8b3dafd5c3ec445f1aa08eb",
      "tree": "755973d3732672cf4dfc47a11be0114a9f5f47cf",
      "parents": [
        "9254ef08d7948888eacc54401775fe5b5890d2c9"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 12:57:19 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 12:57:19 2026 +0200"
      },
      "message": "Parse implicit TLDs in the TldScanner with the strict rule set\n\nJSP.7.3.2 only permits version information and a short name in an\nimplicit.tld file. The web application TLD scanner previously parsed\nthese files with the full TldRuleSet at start-up. That allowed\nelements which the specification forbids in an implicit TLD to take\neffect: a \u003curi\u003e element caused the URI to be registered so that the\nfile could be used for URI based taglib directives, and a \u003clistener\u003e\nelement caused JasperInitializer to register the listener with the\ncontext, aborting start-up when the class could not be instantiated.\n\nTldScanner now owns a second TldParser built with the existing\nImplicitTldRuleSet - the same rule set already used by\nImplicitTagLibraryInfo when a tag file is translated - and uses it\nfor resources whose web application path is an implicit.tld below\n/WEB-INF/tags. An implicit TLD which is not specification compliant\nis now rejected at scan time: the scanner logs a warning naming the\nresource and continues, so such a file can no longer register a URI,\ncache a tag library or contribute listeners, and cannot prevent the\nweb application from starting. If the tag library is actually used\nvia tagdir, the same error is reported at translation time as before.\n\nExplicit taglib mappings in web.xml (scanJspConfig) deliberately keep\nusing the lenient parser since an explicit mapping is an administrator\ndecision and \u003clistener\u003e is legitimate in a regular TLD.\n\nAlso replace the /WEB-INF/tags/ and /implicit.tld literals in\nscanResourcePaths() with the new WEB_INF_TAGS and IMPLICIT_TLD\nconstants, now used by both code paths.\n"
    },
    {
      "commit": "9254ef08d7948888eacc54401775fe5b5890d2c9",
      "tree": "e0ff9ab7d009f7731a5a33e78535f134dd4eef18",
      "parents": [
        "6b2540663f38078ccde3440935e6d6f24231652c"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 12:34:30 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 12:34:30 2026 +0200"
      },
      "message": "Serialize TLD re-parses on the shared parser in TldCache\n\nTldCache shares a single TldParser, and therefore a single Digester,\nacross all cached TLDs. Digester is documented as single-threaded but\nthe re-parse path in getTaglibXml() was only protected by a per-entry\nlock. Two threads recompiling JSPs that referenced two different TLDs\nwhose last modified time had changed since the startup scan could call\ntldParser.parse() concurrently on the same Digester. Observed failure\nmodes included Xerces rejecting the reentrant parse\n(FWK005: parse may not be called while parsing) and a NullPointerException\nafter a concurrent digester.reset() from the finally block of the other\nparse, with the corrupted result being cached until the next change.\n\nFix this by serializing all re-parses on the shared parser\u0027s monitor.\nThe monitor is taken while holding the per-entry lock, and no path\ntakes the two locks in the opposite order, so no deadlock is possible.\nCorrect the class Javadoc, which previously claimed the opposite\nthread-safety behaviour.\n"
    },
    {
      "commit": "6b2540663f38078ccde3440935e6d6f24231652c",
      "tree": "a39f04172b4426f4f3586ba4d2bc8d3f9cefdd3c",
      "parents": [
        "716b7d80663652dab73ed354996257fb84b13130"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 12:19:29 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 12:19:29 2026 +0200"
      },
      "message": "Ignore incomplete tag-plugin entries in TagPluginParser.addPlugin()\n\nThe parser registers addCallMethod for \u003ctag-plugin\u003e with two parameters\ntaken from the child element bodies. CallMethodRule always invokes the\nmethod when it expects more than one parameter, passing null for\nparameters whose element is absent. As a result, a \u003ctag-plugin\u003e element\nthat omitted \u003ctag-class\u003e or \u003cplugin-class\u003e stored a map entry with a\nnull key or value.\n\nThe sole consumer, Jasper\u0027s TagPluginManager, then either called\nloadClass(null), which produced a raw NullPointerException surfaced as\nan unhelpful JasperException during JSP translation, or silently stored\nan entry under a null key that could never match a tag handler class.\n\nFix this by ignoring entries with a null or empty tag class or plugin\nclass and logging a localized warning via StringManager, using a new\nresource bundle for the org.apache.tomcat.util.descriptor.tagplugin\npackage. Complete entries in the same descriptor are still registered.\nDocument the behaviour in the addPlugin() Javadoc. Add tests covering\ndirect calls with missing values and the parsing of a descriptor\nmixing incomplete and complete entries.\n"
    },
    {
      "commit": "716b7d80663652dab73ed354996257fb84b13130",
      "tree": "fc9ab3409663e5a7567efb7a24c1f3ab1815ca54",
      "parents": [
        "ea48eaaa3c05f7821d3bd3100fd82035e4aff2c6"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 12:12:21 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 12:12:21 2026 +0200"
      },
      "message": "Fix missing path separator in XmlIdentifiers.WEBSERVICES_12_XSD\n\nThe constant concatenated the Java EE 5 namespace and the schema file\nname without the \u0027/\u0027 separator, producing the malformed URL\nhttp://java.sun.com/xml/ns/javaeejavaee_web_services_1_2.xsd instead of\nhttp://java.sun.com/xml/ns/javaee/javaee_web_services_1_2.xsd used by\nthe specification and by DigesterFactory when registering the schema.\n\nThe constant is currently unreferenced - DigesterFactory registers the\nweb services schemas via bare file names - so there is no functional\nimpact today. The correction removes a latent copy-paste defect from\nthis public constants class and aligns the value with all sibling\nconstants, which use the namespace plus \"/filename\".\n"
    },
    {
      "commit": "ea48eaaa3c05f7821d3bd3100fd82035e4aff2c6",
      "tree": "8285729905ec23db5bb0da45210feb8f0c9533a6",
      "parents": [
        "b760e70832bddeb4c63d425954b1b8315a8e5c30"
      ],
      "author": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 12:10:12 2026 +0200"
      },
      "committer": {
        "name": "opencode",
        "email": "remm@apache.org",
        "time": "Wed Oct 14 12:10:12 2026 +0200"
      },
      "message": "Make CaseInsensitiveKeyMap.keySet() view fully case-insensitive\n\nThe class extends AbstractMap and only overrode entrySet(), so keySet()\nreturned the AbstractMap view whose contains() delegates to the map\u0027s\ncase-insensitive containsKey() while remove(Object) iterates the keys\ncomparing them with String.equals(). For a stored key \"a\",\nkeySet().contains(\"A\") returned true but keySet().remove(\"A\")\nreturned false and left the entry in place, violating the Set contract\nfor callers that remove keys via the view with different casing.\n\nOverride keySet() with a KeySet view backed by the same live map as the\nentry set. Its iterator reports keys in their original case, contains()\nand remove() are case-insensitive, removal correctly reports success\neven for entries with a null value, null and non-String arguments return\nfalse, and clear() empties the backing map. Non-String and null keys are\nnever stored since put() rejects null keys and Key.getInstance() returns\nnull for other types.\n\nAdd tests for case-insensitive removal of a set object, rejection of\nnull and non-String keys, original-case keys from the iterator and\nclearing via the view.\n"
    }
  ],
  "next": "b760e70832bddeb4c63d425954b1b8315a8e5c30"
}
