This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit a9aa2f7871d0026bb16c133097dc59b6f5226f0d Author: opencode <[email protected]> AuthorDate: Sun Oct 11 14:53:49 2026 +0200 Deserialize the key of a backup request with the loaders of the map Answering a backup request read the key of the received message without specifying class loaders, unlike every other receive path which deserializes with the loaders of the map. A key whose class is not visible to the default loaders then failed the reply with an unchecked exception on the responder, which left the requester without an answer until its rpc timeout. The request is now deserialized with the loaders of the map before the lookup, and a key that cannot be deserialized fails the request with a plain error log and no reply, the same outcome as for a missing entry. --- .../tribes/tipis/AbstractReplicatedMap.java | 11 +++- .../tribes/tipis/TestAbstractReplicatedMap.java | 70 ++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java b/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java index 3ef84d7777..8ecf02efd3 100644 --- a/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java +++ b/java/org/apache/catalina/tribes/tipis/AbstractReplicatedMap.java @@ -700,7 +700,16 @@ public abstract class AbstractReplicatedMap<K, V> // backup request if (mapmsg.getMsgType() == MapMessage.MSG_RETRIEVE_BACKUP) { - MapEntry<K,V> entry = innerMap.get(mapmsg.getKey()); + MapEntry<K,V> entry; + try { + // Deserialize the key with the external loaders, like the other receive paths do, + // since the default loaders cannot see the class of a key from an application + mapmsg.deserialize(getExternalLoaders()); + entry = innerMap.get(mapmsg.getKey()); + } catch (ClassNotFoundException | IOException x) { + log.error(sm.getString("abstractReplicatedMap.unable.deserialize.MapMessage"), x); + return null; + } if (entry == null || (!entry.isSerializable())) { return null; } diff --git a/test/org/apache/catalina/tribes/tipis/TestAbstractReplicatedMap.java b/test/org/apache/catalina/tribes/tipis/TestAbstractReplicatedMap.java index ae82476a55..0f3673def6 100644 --- a/test/org/apache/catalina/tribes/tipis/TestAbstractReplicatedMap.java +++ b/test/org/apache/catalina/tribes/tipis/TestAbstractReplicatedMap.java @@ -35,6 +35,7 @@ import org.apache.catalina.tribes.ErrorHandler; import org.apache.catalina.tribes.Member; import org.apache.catalina.tribes.UniqueId; import org.apache.catalina.tribes.group.GroupChannel; +import org.apache.catalina.tribes.io.XByteBuffer; import org.apache.catalina.tribes.membership.MemberImpl; import org.apache.catalina.tribes.tipis.AbstractReplicatedMap.MapMessage; @@ -266,4 +267,73 @@ public class TestAbstractReplicatedMap { Assert.assertFalse(map.isEmpty()); Assert.assertTrue(map.values().contains(null)); } + + private static final class SerialKey implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private final String id; + + SerialKey(String id) { + this.id = id; + } + + @Override + public boolean equals(Object o) { + return o instanceof SerialKey && id.equals(((SerialKey) o).id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + } + + /* + * A received backup request carries its key only in serialized form. The reply must deserialize + * that key with the loaders of the map, like the other receive paths do, and must fail the + * request without throwing when the key cannot be deserialized, so that the requester gets a + * normal timeout instead of being answered by a failure of the responder. + */ + @Test + public void testBackupRequestDeserializesKeyOfReceivedMessage() throws Exception { + LazyReplicatedMap<Object,String> map = new LazyReplicatedMap<Object,String>( + (key, value) -> { + // Not made primary here + }, new TestChannel(), 1000, "TestAbstractReplicatedMap", null); + map.put(new SerialKey("key"), "value"); + Member sender = new MemberImpl("localhost", 4017, -1); + + // Round trip over the serialization to get a message with the key data but no live key + byte[] wire = XByteBuffer.serialize(new MapMessage( + "TestAbstractReplicatedMap".getBytes(StandardCharsets.UTF_8), MapMessage.MSG_RETRIEVE_BACKUP, false, + new SerialKey("key"), null, null, null, null)); + + MapMessage received = (MapMessage) XByteBuffer.deserialize(wire); + Serializable reply = map.replyRequest(received, sender); + Assert.assertTrue(reply instanceof MapMessage); + Assert.assertEquals("value", ((MapMessage) reply).getValue()); + + // Corrupt the class name inside the serialized key, so it cannot be deserialized + byte[] corrupted = wire.clone(); + int at = indexOf(corrupted, "SerialKey".getBytes(StandardCharsets.US_ASCII)); + Assert.assertTrue(at > 0); + corrupted[at] = 'X'; + MapMessage broken = (MapMessage) XByteBuffer.deserialize(corrupted); + Assert.assertNull(map.replyRequest(broken, sender)); + } + + private static int indexOf(byte[] data, byte[] pattern) { + outer: + for (int i = 0; i <= data.length - pattern.length; i++) { + for (int j = 0; j < pattern.length; j++) { + if (data[i + j] != pattern[j]) { + continue outer; + } + } + return i; + } + return -1; + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
