From 73d6cb715504720e1a0f39a2837a5cb5f00e1496 Mon Sep 17 00:00:00 2001 From: hexonal Date: Mon, 3 Aug 2026 01:21:52 -0400 Subject: [PATCH] Fix LREM with count = int.MinValue killing the session ListRemove normalizes a negative count with Math.Abs, which throws OverflowException for int.MinValue ("Negating the minimum value of a twos complement number is invalid"). The exception unwinds out of ProcessMessages and terminates the RESP session: the client gets no reply and a reset connection. The server process stays up and other sessions are unaffected. Reproduced against unmodified main: RPUSH L a b c LREM L -2147483648 a -> connection closed, no reply Clamp instead of negating. The removal loop is bounded by removedCount < count with removedCount never exceeding the list length, which cannot exceed int.MaxValue, so int.MaxValue removes exactly the same elements that a correctly negated 2147483648 would. Scope: the other Math.Abs call sites on a client-supplied count (HRANDFIELD, SRANDMEMBER, ZRANDMEMBER) were checked with the same input and none of them terminates the session, so this is the only instance and the fix stays at one line. Verified on macOS arm64, net10.0 Release: the new test fails without the change with RedisConnectionException SocketClosed, and passes with it. RespListTests: 104/104. net8.0 builds clean; its tests were not run here, no net8.0 runtime in this environment. --- libs/server/Objects/List/ListObjectImpl.cs | 5 ++++- .../Garnet.test.collections/RespListTests.cs | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/libs/server/Objects/List/ListObjectImpl.cs b/libs/server/Objects/List/ListObjectImpl.cs index 561c1a0ece1..c26ab4df38d 100644 --- a/libs/server/Objects/List/ListObjectImpl.cs +++ b/libs/server/Objects/List/ListObjectImpl.cs @@ -49,7 +49,10 @@ private void ListRemove(ref ObjectInput input, ref ObjectOutput output) var fromHeadToTail = count > 0; var currentNode = fromHeadToTail ? list.First : list.Last; - count = Math.Abs(count); + // |int.MinValue| does not fit in an int, so Math.Abs would throw. + // The loop below is bounded by the list length, which cannot exceed + // int.MaxValue, so clamping removes exactly the same elements. + count = count == int.MinValue ? int.MaxValue : Math.Abs(count); while (removedCount < count && currentNode != null) { var nextNode = fromHeadToTail ? currentNode.Next : currentNode.Previous; diff --git a/test/standalone/Garnet.test.collections/RespListTests.cs b/test/standalone/Garnet.test.collections/RespListTests.cs index 1e11eef1b5a..86349f549b1 100644 --- a/test/standalone/Garnet.test.collections/RespListTests.cs +++ b/test/standalone/Garnet.test.collections/RespListTests.cs @@ -371,6 +371,28 @@ public void BasicRPUSHAndLREM() ClassicAssert.IsFalse(exists); } + [Test] + public void LREMWithIntMinValueCountRemovesAllMatches() + { + using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig()); + var db = redis.GetDatabase(0); + + var key = "List_Test_LREM_MinValue"; + db.KeyDelete(key); + db.ListRightPush(key, ["a", "b", "a", "c", "a"]); + + // |int.MinValue| does not fit in an int; before the fix Math.Abs threw + // OverflowException out of ProcessMessages and the session was dropped. + var removed = db.ListRemove(key, "a", int.MinValue); + ClassicAssert.AreEqual(3, removed); + + var remaining = db.ListRange(key, 0, -1); + ClassicAssert.AreEqual(new RedisValue[] { "b", "c" }, remaining); + + // The connection must still be usable. + ClassicAssert.AreEqual("PONG", db.Execute("PING").ToString()); + } + [Test] public void MultiLPUSHAndLPOPV1() {