You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kudu.apache.org by al...@apache.org on 2026/06/09 20:02:16 UTC

(kudu) branch master updated: [subprocess] Fix buffer over-read/overflow in message discard

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kudu.git


The following commit(s) were added to refs/heads/master by this push:
     new c968d85cc [subprocess] Fix buffer over-read/overflow in message discard
c968d85cc is described below

commit c968d85cc9d5ea18cbabf7464cb48bbec51a63a6
Author: Ashwani <ar...@gmail.com>
AuthorDate: Tue Jun 9 21:09:43 2026 +0530

    [subprocess] Fix buffer over-read/overflow in message discard
    
    SubprocessProtocol::DoReadAndDiscard() clears an oversized message off
    the communication channel using a fixed 4K stack buffer, but bounded
    each read() with std::max<ssize_t>(rem, sizeof(buf)) instead of
    std::min(). Since this returns the greater of the two, the read length
    is never held to the buffer size. This causes two issues:
    * Stack buffer overflow: when the discarded payload is larger than
      4K (say, 8K message), a single read() is asked to copy up to 'rem'
      bytes into the 4K buffer, smashing the stack.
    * Valid message subsumption: when the oversized payload is smaller than
      4K, the read still pulls up to 4K, over-reading into and consuming the
      following well-formed message. The next ReceiveMessage() then loses
      that message, breaking the "discard to re-sync the channel" guarantee.
    
    The bug has existed since DoReadAndDiscard() was introduced in KUDU-3450.
    The patch fixes it by limiting each read to std::min(rem, sizeof(buf)),
    matching the Java implementation in doReadAndDiscard.
    
    Add two unit tests covering both scenarios: a deterministic over-read
    and an oversized-payload overflow test.
    
    Change-Id: I73da816132555cb3d29b7bbf8a6821ad3f2dec9c
    Reviewed-on: http://gerrit.cloudera.org:8080/24427
    Reviewed-by: Alexey Serbin <al...@apache.org>
    Tested-by: Alexey Serbin <al...@apache.org>
---
 src/kudu/subprocess/subprocess_protocol.cc    |   2 +-
 src/kudu/subprocess/subprocess_server-test.cc | 103 ++++++++++++++++++++++++++
 2 files changed, 104 insertions(+), 1 deletion(-)

diff --git a/src/kudu/subprocess/subprocess_protocol.cc b/src/kudu/subprocess/subprocess_protocol.cc
index 42667bb3c..943ec2080 100644
--- a/src/kudu/subprocess/subprocess_protocol.cc
+++ b/src/kudu/subprocess/subprocess_protocol.cc
@@ -210,7 +210,7 @@ Status SubprocessProtocol::DoReadAndDiscard(ssize_t size) const {
   ssize_t rem = size;
   while (rem > 0) {
     ssize_t r;
-    RETRY_ON_EINTR(r, read(read_fd_, buf, std::max<ssize_t>(rem, sizeof(buf))));
+    RETRY_ON_EINTR(r, read(read_fd_, buf, std::min<ssize_t>(rem, sizeof(buf))));
     if (r == -1) {
       const int err = errno;
       return Status::IOError("Error reading from pipe", "", err);
diff --git a/src/kudu/subprocess/subprocess_server-test.cc b/src/kudu/subprocess/subprocess_server-test.cc
index b6e234a50..1f8c94462 100644
--- a/src/kudu/subprocess/subprocess_server-test.cc
+++ b/src/kudu/subprocess/subprocess_server-test.cc
@@ -15,6 +15,9 @@
 // specific language governing permissions and limitations
 // under the License.
 
+#include <unistd.h>
+
+#include <cstdint>
 #include <memory>
 #include <ostream>
 #include <string>
@@ -33,6 +36,7 @@
 #include "kudu/subprocess/echo_subprocess.h"
 #include "kudu/subprocess/server.h"
 #include "kudu/subprocess/subprocess.pb.h"
+#include "kudu/subprocess/subprocess_protocol.h"
 #include "kudu/util/env.h"
 #include "kudu/util/metrics.h"
 #include "kudu/util/monotime.h"
@@ -466,6 +470,105 @@ TEST_F(SubprocessServerTest, LargePayloadSize) {
   }
 }
 
+// Regression test for the buffer-overflow / over-read bug in
+// SubprocessProtocol::DoReadAndDiscard(), which used
+// std::max<ssize_t>(rem, sizeof(buf)) for read instead of std::min().
+// These tests drive SubprocessProtocol directly over a pipe (no Java
+// subprocess involved) so the oversized-message discard path is
+// exercised deterministically.
+class SubprocessProtocolTest : public KuduTest {};
+
+// When an oversized message is discarded, DoReadAndDiscard() must consume
+// *exactly* the oversized payload and no more. With the 'std::max' bug, the
+// discard read pulls up to sizeof(buf) (4096) bytes even when the oversized
+// payload is smaller, swallowing the bytes of the *following* well-formed
+// message and desynchronizing the channel.
+//
+// This test fails deterministically against the buggy code (the follow-up
+// message is lost and the next read hits EOF) and passes once the read is
+// bounded by std::min().
+TEST_F(SubprocessProtocolTest, OversizedMessageDoesNotOverReadFollowingMessage) {
+  int fds[2];
+  PCHECK(pipe(fds) == 0);
+  const int read_fd = fds[0];
+  const int write_fd = fds[1];
+  SCOPED_CLEANUP({ close(read_fd); });
+
+  // Cap the reader at 150 bytes: large enough to accept a tiny request, small
+  // enough to reject one carrying a few hundred payload bytes.
+  constexpr uint32_t kMaxMsgBytes = 150;
+
+  // The oversized payload is intentionally well under sizeof(buf) (4096) so
+  // that the buggy std::max() read over-reads into the following message
+  // rather than overflowing the stack buffer (that case is covered below).
+  SubprocessRequestPB send_oversized_req = CreateEchoSubprocessRequestPB(string(300, 'x'));
+  SubprocessRequestPB send_valid_req = CreateEchoSubprocessRequestPB("legit request");
+
+  {
+    SubprocessProtocol writer(SubprocessProtocol::SerializationMode::PB,
+                              SubprocessProtocol::CloseMode::NO_CLOSE_ON_DESTROY,
+                              -1, write_fd, 0);
+    ASSERT_OK(writer.SendMessage(send_oversized_req));
+    ASSERT_OK(writer.SendMessage(send_valid_req));
+  }
+  // Close the write end so that, if the follow-up message is wrongly consumed
+  // while discarding, the reader observes EOF instead of blocking forever.
+  PCHECK(close(write_fd) == 0);
+
+  SubprocessProtocol reader(SubprocessProtocol::SerializationMode::PB,
+                            SubprocessProtocol::CloseMode::NO_CLOSE_ON_DESTROY,
+                            read_fd, -1, kMaxMsgBytes);
+
+  // The first message is rejected as oversized; its payload must be discarded.
+  SubprocessRequestPB received_oversized_req;
+  Status s = reader.ReceiveMessage(&received_oversized_req);
+  ASSERT_TRUE(s.IsIOError()) << s.ToString();
+  ASSERT_STR_CONTAINS(s.ToString(), "exceeds maximum message size");
+
+  // The following well-formed message must still be intact. With the bug it
+  // is swallowed during the discard, so this read returns EOF.
+  SubprocessRequestPB received_valid_req;
+  ASSERT_OK(reader.ReceiveMessage(&received_valid_req));
+  EchoRequestPB echo;
+  ASSERT_TRUE(received_valid_req.request().UnpackTo(&echo));
+  ASSERT_EQ("legit request", echo.data());
+}
+
+// When the oversized payload is larger than the 4096-byte stack buffer, the buggy
+// 'std::max()' limit check makes read() to copy 'rem' (the full payload size) bytes
+// into a 4096-byte stack array in a single call, smashing the stack. This is
+// caught by ASAN. With the 'std::min()' fix the payload is discarded in bounded
+// chunks and the call simply reports the oversized error. The std::min limit
+// aligns with java implementation of doReadAndDiscard as well.
+TEST_F(SubprocessProtocolTest, OversizedMessageDiscardDoesNotOverflowStackBuffer) {
+  int fds[2];
+  PCHECK(pipe(fds) == 0);
+  const int read_fd = fds[0];
+  const int write_fd = fds[1];
+  SCOPED_CLEANUP({ close(read_fd); });
+
+  constexpr uint32_t kMaxMsgBytes = 150;
+  // 8K payload: comfortably larger than the 4096-byte buffer so a single read()
+  // call flows over it.
+  SubprocessRequestPB send_oversized_req = CreateEchoSubprocessRequestPB(string(8 * 1024, 'x'));
+
+  {
+    SubprocessProtocol writer(SubprocessProtocol::SerializationMode::PB,
+                              SubprocessProtocol::CloseMode::NO_CLOSE_ON_DESTROY,
+                              -1, write_fd, 0);
+    ASSERT_OK(writer.SendMessage(send_oversized_req));
+  }
+  PCHECK(close(write_fd) == 0);
+
+  SubprocessProtocol reader(SubprocessProtocol::SerializationMode::PB,
+                            SubprocessProtocol::CloseMode::NO_CLOSE_ON_DESTROY,
+                            read_fd, -1, kMaxMsgBytes);
+  SubprocessRequestPB got;
+  Status s = reader.ReceiveMessage(&got);
+  ASSERT_TRUE(s.IsIOError()) << s.ToString();
+  ASSERT_STR_CONTAINS(s.ToString(), "exceeds maximum message size");
+}
+
 } // namespace subprocess
 } // namespace kudu