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

chenBright pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git


The following commit(s) were added to refs/heads/master by this push:
     new 49f8d6f1 Fix empty %b/%s argument dropped in redis command formatting 
(#3566)
49f8d6f1 is described below

commit 49f8d6f151ddbde6a2302d69a80479bc38fcac93
Author: NeutralMilkHotel <[email protected]>
AuthorDate: Wed Oct 7 17:54:10 2026 +0800

    Fix empty %b/%s argument dropped in redis command formatting (#3566)
    
    * Fix empty %b/%s argument dropped in redis command formatting (#2275)
    
    * Fix %s/%b variadic argument type mismatch in RedisCommandFormatV
    
    * Fix %b binary test to compare full payload bytes instead of ASSERT_STREQ
    
    * Tighten va_arg const char* comment in RedisCommandFormatV
    
    * Return false instead of CHECK-abort on AddCommand format errors
---
 src/brpc/redis.cpp           |  8 ++--
 src/brpc/redis_command.cpp   | 35 ++++++++++++++---
 test/brpc_redis_unittest.cpp | 94 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 127 insertions(+), 10 deletions(-)

diff --git a/src/brpc/redis.cpp b/src/brpc/redis.cpp
index 050f28d7..a64e60bb 100644
--- a/src/brpc/redis.cpp
+++ b/src/brpc/redis.cpp
@@ -98,7 +98,7 @@ bool RedisRequest::AddCommand(const butil::StringPiece& 
command) {
         ++_ncommand;
         return true;
     } else {
-        CHECK(st.ok()) << st;
+        LOG(ERROR) << "RedisRequest::AddCommand failed: " << st;
         _has_error = true;
         return false;
     }
@@ -114,7 +114,7 @@ bool RedisRequest::AddCommandByComponents(const 
butil::StringPiece* components,
         ++_ncommand;
         return true;
     } else {
-        CHECK(st.ok()) << st;
+        LOG(ERROR) << "RedisRequest::AddCommand failed: " << st;
         _has_error = true;
         return false;
     }
@@ -137,7 +137,7 @@ bool RedisRequest::AddCommandWithArgs(const char* fmt, ...) 
{
         ++_ncommand;
         return true;
     } else {
-        CHECK(st.ok()) << st;
+        LOG(ERROR) << "RedisRequest::AddCommand failed: " << st;
         _has_error = true;
         return false;
     }
@@ -152,7 +152,7 @@ bool RedisRequest::AddCommandV(const char* fmt, va_list ap) 
{
         ++_ncommand;
         return true;
     } else {
-        CHECK(st.ok()) << st;
+        LOG(ERROR) << "RedisRequest::AddCommand failed: " << st;
         _has_error = true;
         return false;
     }
diff --git a/src/brpc/redis_command.cpp b/src/brpc/redis_command.cpp
index c0ff6477..2513d160 100644
--- a/src/brpc/redis_command.cpp
+++ b/src/brpc/redis_command.cpp
@@ -114,20 +114,25 @@ RedisCommandFormatV(butil::IOBuf* outbuf, const char* 
fmt, va_list ap) {
     char quote_char = 0;
     const char* quote_pos = fmt;
     int nargs = 0;
+    // Set after consuming %s/%b so that an empty arg still forms a component
+    // (e.g. "set key %b" with an empty value must emit 3 components, not 2).
+    bool format_arg_in_comp = false;
     for (; *c; ++c) {
         if (*c != '%' || c[1] == '\0') {
             if (*c == ' ') {
                 if (quote_char) {
                     compbuf.push_back(*c);
-                } else if (!compbuf.empty()) {
+                } else if (!compbuf.empty() || format_arg_in_comp) {
                     FlushComponent(&nocount_buf, &compbuf, &ncomponent);
+                    format_arg_in_comp = false;
                 }
             } else if (*c == '"' || *c == '\'') {  // Check quotation.
                 if (!quote_char) {  // begin quote
                     quote_char = *c;
                     quote_pos = c;
-                    if (!compbuf.empty()) {
+                    if (!compbuf.empty() || format_arg_in_comp) {
                         FlushComponent(&nocount_buf, &compbuf, &ncomponent);
+                        format_arg_in_comp = false;
                     }
                 } else if (quote_char == *c) {
                     const char last_char = (compbuf.empty() ? 0 : 
compbuf.back());
@@ -140,6 +145,7 @@ RedisCommandFormatV(butil::IOBuf* outbuf, const char* fmt, 
va_list ap) {
                     } else { // end quote
                         quote_char = 0;
                         FlushComponent(&nocount_buf, &compbuf, &ncomponent);
+                        format_arg_in_comp = false;
                     }
                 } else {
                     compbuf.push_back(*c);
@@ -148,24 +154,40 @@ RedisCommandFormatV(butil::IOBuf* outbuf, const char* 
fmt, va_list ap) {
                 compbuf.push_back(*c);
             }
         } else {
-            char *arg;
+            const char* arg;
             size_t size;
 
             switch(c[1]) {
             case 's':
-                arg = va_arg(ap, char*);
+                // Most callers pass const char* (c_str()/data()); reading as
+                // const char* matches them exactly. char* is ABI-identical to
+                // const char*, so mutable callers stay correct in practice; a
+                // single va_arg cannot serve both types exactly.
+                arg = va_arg(ap, const char*);
+                // strlen(NULL) is UB; forbid it explicitly instead of 
crashing.
+                if (arg == nullptr) {
+                    return butil::Status(EINVAL, "%%s argument is NULL");
+                }
                 size = strlen(arg);
                 if (size > 0) {
                     compbuf.append(arg, size);
                 }
+                format_arg_in_comp = true;
                 ++nargs;
                 break;
             case 'b':
-                arg = va_arg(ap, char*);
+                arg = va_arg(ap, const char*);
                 size = va_arg(ap, size_t);
+                // append(NULL, n>0) is UB; size==0 with NULL is a valid empty.
+                if (arg == nullptr && size > 0) {
+                    return butil::Status(EINVAL,
+                                         "%%b argument is NULL with size=%lu",
+                                         (unsigned long)size);
+                }
                 if (size > 0) {
                     compbuf.append(arg, size);
                 }
+                format_arg_in_comp = true;
                 ++nargs;
                 break;
             case '%':
@@ -300,8 +322,9 @@ RedisCommandFormatV(butil::IOBuf* outbuf, const char* fmt, 
va_list ap) {
                              (int)ctx_size, ctx_begin, quote_pos - fmt);
     }
     
-    if (!compbuf.empty()) {
+    if (!compbuf.empty() || format_arg_in_comp) {
         FlushComponent(&nocount_buf, &compbuf, &ncomponent);
+        format_arg_in_comp = false;
     }
 
     LOG_IF(ERROR, nargs == 0) << "You must call RedisCommandNoFormat() "
diff --git a/test/brpc_redis_unittest.cpp b/test/brpc_redis_unittest.cpp
index 67ab8b98..610ea2ea 100644
--- a/test/brpc_redis_unittest.cpp
+++ b/test/brpc_redis_unittest.cpp
@@ -577,6 +577,100 @@ TEST_F(RedisTest, cmd_format) {
     request.AddCommand("  get   key'ext'   value  ");  // == get key ext value
     
ASSERT_STREQ("*4\r\n$3\r\nget\r\n$3\r\nkey\r\n$3\r\next\r\n$5\r\nvalue\r\n", 
request._buf.to_string().c_str());
     request.Clear();
+
+    // empty %b must still form a component (issue: empty arg was dropped)
+    {
+        std::string empty;
+        request.AddCommand("set key %b", empty.data(), empty.size());
+        ASSERT_STREQ("*3\r\n$3\r\nset\r\n$3\r\nkey\r\n$0\r\n\r\n",
+                     request._buf.to_string().c_str());
+        request.Clear();
+    }
+    // empty %s must still form a component
+    {
+        std::string empty;
+        request.AddCommand("set key %s", empty.c_str());
+        ASSERT_STREQ("*3\r\n$3\r\nset\r\n$3\r\nkey\r\n$0\r\n\r\n",
+                     request._buf.to_string().c_str());
+        request.Clear();
+    }
+    // %b with binary data containing \0
+    {
+        const char bin[] = {'a', '\0', 'b'};
+        request.AddCommand("set key %b", bin, (size_t)3);
+        // Compare full bytes: ASSERT_STREQ would stop at the embedded NUL and
+        // miss a payload truncated after 'a'.
+        std::string expected = "*3\r\n$3\r\nset\r\n$3\r\nkey\r\n$3\r\n";
+        expected.append(bin, sizeof(bin));
+        expected.append("\r\n", 2);
+        ASSERT_EQ(expected, request._buf.to_string());
+        request.Clear();
+    }
+}
+
+// Format-only checks: do not need a running redis-server.
+TEST_F(RedisTest, empty_and_null_format_args) {
+    butil::IOBuf buf;
+    const std::string empty;
+
+    // empty %b still forms a component
+    ASSERT_TRUE(
+        brpc::RedisCommandFormat(&buf, "set key %b", empty.data(), 
empty.size())
+            .ok());
+    ASSERT_STREQ("*3\r\n$3\r\nset\r\n$3\r\nkey\r\n$0\r\n\r\n",
+                 buf.to_string().c_str());
+    buf.clear();
+
+    // empty %s still forms a component
+    ASSERT_TRUE(
+        brpc::RedisCommandFormat(&buf, "set key %s", empty.c_str()).ok());
+    ASSERT_STREQ("*3\r\n$3\r\nset\r\n$3\r\nkey\r\n$0\r\n\r\n",
+                 buf.to_string().c_str());
+    buf.clear();
+
+    // %b with NULL + size 0 is a valid empty arg (no deref)
+    ASSERT_TRUE(brpc::RedisCommandFormat(&buf, "set key %b",
+                                         (const char*)nullptr, (size_t)0)
+                    .ok());
+    ASSERT_STREQ("*3\r\n$3\r\nset\r\n$3\r\nkey\r\n$0\r\n\r\n",
+                 buf.to_string().c_str());
+    buf.clear();
+
+    // %s with NULL must report an error instead of strlen(UB)/crash
+    ASSERT_FALSE(
+        brpc::RedisCommandFormat(&buf, "set key %s", (const char*)nullptr)
+            .ok());
+    buf.clear();
+
+    // %b with NULL + size>0 must report an error instead of append(UB)/crash
+    ASSERT_FALSE(brpc::RedisCommandFormat(&buf, "set key %b",
+                                          (const char*)nullptr, (size_t)3)
+                     .ok());
+    buf.clear();
+
+    // multiple consecutive empty args: no state crosstalk, exact count
+    ASSERT_TRUE(brpc::RedisCommandFormat(
+                    &buf, "mset %b %b %b", empty.data(), empty.size(),
+                    empty.data(), empty.size(), empty.data(), empty.size())
+                    .ok());
+    ASSERT_STREQ("*4\r\n$4\r\nmset\r\n$0\r\n\r\n$0\r\n\r\n$0\r\n\r\n",
+                 buf.to_string().c_str());
+    buf.clear();
+}
+
+// NULL through the public AddCommand path must return false / set has_error,
+// not abort the process (the failure branch used to CHECK and crash).
+TEST_F(RedisTest, addcommand_null_returns_false) {
+    brpc::RedisRequest request;
+
+    ASSERT_FALSE(request.AddCommand("set key %s", (const char*)nullptr));
+    ASSERT_TRUE(request.has_error());
+    ASSERT_FALSE(request.AddCommand("ping"));  // sticky: still false after 
error
+
+    request.Clear();
+    ASSERT_FALSE(
+        request.AddCommand("set key %b", (const char*)nullptr, (size_t)3));
+    ASSERT_TRUE(request.has_error());
 }
 
 TEST_F(RedisTest, quote_and_escape) {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to