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

nic-6443 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 6c988bc9d fix(core/etcd): nil-deref on response without header field 
(#13361)
6c988bc9d is described below

commit 6c988bc9dbba787404673074948efcf19ad455ef
Author: Nic <[email protected]>
AuthorDate: Wed May 13 12:01:28 2026 +0800

    fix(core/etcd): nil-deref on response without header field (#13361)
---
 apisix/core/config_etcd.lua |  11 ++++-
 apisix/core/etcd.lua        |  44 ++++++++++++++++---
 t/core/etcd-nil-header.t    | 104 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 152 insertions(+), 7 deletions(-)

diff --git a/apisix/core/config_etcd.lua b/apisix/core/config_etcd.lua
index d5753c5e8..a56a221b6 100644
--- a/apisix/core/config_etcd.lua
+++ b/apisix/core/config_etcd.lua
@@ -163,9 +163,18 @@ local function do_run_watch(premature)
                 if not res then
                     log.error("etcd get: ", err)
                     ngx_sleep(3)
+                elseif not (res.body and res.body.header and 
res.body.header.revision) then
+                    log.error("etcd response missing header.revision")
+                    ngx_sleep(3)
                 else
                     rev = tonumber(res.body.header.revision)
-                    break
+                    if not rev then
+                        log.error("etcd response has invalid header.revision: 
",
+                                  tostring(res.body.header.revision))
+                        ngx_sleep(3)
+                    else
+                        break
+                    end
                 end
             end
         end
diff --git a/apisix/core/etcd.lua b/apisix/core/etcd.lua
index 3caa2f1cc..09a6b45f4 100644
--- a/apisix/core/etcd.lua
+++ b/apisix/core/etcd.lua
@@ -257,6 +257,15 @@ local function not_found(res)
 end
 
 
+local function get_header_revision(res)
+    local header = res.body.header
+    if not (header and header.revision) then
+        return nil, "etcd response missing header.revision"
+    end
+    return header.revision
+end
+
+
 -- When `is_dir` is true, returns the value of both the dir key and its 
descendants.
 -- Otherwise, return the value of key only.
 function _M.get_format(res, real_key, is_dir, formatter)
@@ -273,7 +282,11 @@ function _M.get_format(res, real_key, is_dir, formatter)
         return nil, res.body.error
     end
 
-    res.headers["X-Etcd-Index"] = res.body.header.revision
+    local revision, err = get_header_revision(res)
+    if not revision then
+        return nil, err
+    end
+    res.headers["X-Etcd-Index"] = revision
 
     if not res.body.kvs then
         return not_found(res)
@@ -452,7 +465,11 @@ local function set(key, value, ttl)
         return nil, res.body.error
     end
 
-    res.headers["X-Etcd-Index"] = res.body.header.revision
+    local revision, rev_err = get_header_revision(res)
+    if not revision then
+        return nil, rev_err
+    end
+    res.headers["X-Etcd-Index"] = revision
 
     -- etcd v3 set would not return kv info
     v3_adapter.to_v3(res.body, "set")
@@ -521,7 +538,11 @@ function _M.atomic_set(key, value, ttl, mod_revision)
         return nil, "value changed before overwritten"
     end
 
-    res.headers["X-Etcd-Index"] = res.body.header.revision
+    local revision, rev_err = get_header_revision(res)
+    if not revision then
+        return nil, rev_err
+    end
+    res.headers["X-Etcd-Index"] = revision
     -- etcd v3 set would not return kv info
     v3_adapter.to_v3(res.body, "compareAndSwap")
     res.body.node = {
@@ -555,7 +576,10 @@ function _M.push(key, value, ttl)
     end
 
     -- manually add suffix
-    local index = res.body.header.revision
+    local index, rev_err = get_header_revision(res)
+    if not index then
+        return nil, rev_err
+    end
     index = string.format("%020d", index)
 
     -- set the basic id attribute
@@ -588,7 +612,11 @@ function _M.delete(key)
         return nil, err
     end
 
-    res.headers["X-Etcd-Index"] = res.body.header.revision
+    local revision, rev_err = get_header_revision(res)
+    if not revision then
+        return nil, rev_err
+    end
+    res.headers["X-Etcd-Index"] = revision
 
     if not res.body.deleted then
         return not_found(res), nil
@@ -618,7 +646,11 @@ function _M.rmdir(key, opts)
         return nil, err
     end
 
-    res.headers["X-Etcd-Index"] = res.body.header.revision
+    local revision, rev_err = get_header_revision(res)
+    if not revision then
+        return nil, rev_err
+    end
+    res.headers["X-Etcd-Index"] = revision
 
     if not res.body.deleted then
         return not_found(res), nil
diff --git a/t/core/etcd-nil-header.t b/t/core/etcd-nil-header.t
new file mode 100644
index 000000000..e770f830a
--- /dev/null
+++ b/t/core/etcd-nil-header.t
@@ -0,0 +1,104 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_shuffle();
+no_root_location();
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+    if (!defined $block->ignore_error_log) {
+        $block->set_value("ignore_error_log", "");
+    }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: get_format returns error when body has no header field
+--- config
+    location /t {
+        content_by_lua_block {
+            local etcd_apisix = require("apisix.core.etcd")
+            local res = {
+                headers = {},
+                body = {
+                    kvs = {{key = "/test", value = "v"}},
+                },
+            }
+            local ok, err = etcd_apisix.get_format(res, "/test", false)
+            ngx.say("ok: ", ok ~= nil)
+            ngx.say("err: ", err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+ok: false
+err: etcd response missing header.revision
+
+
+
+=== TEST 2: get_format returns error when body is an empty table
+--- config
+    location /t {
+        content_by_lua_block {
+            local etcd_apisix = require("apisix.core.etcd")
+            local res = {
+                headers = {},
+                body = {},
+            }
+            local ok, err = etcd_apisix.get_format(res, "/test", false)
+            ngx.say("ok: ", ok ~= nil)
+            ngx.say("err: ", err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+ok: false
+err: etcd response missing header.revision
+
+
+
+=== TEST 3: get_format succeeds when header.revision is present
+--- config
+    location /t {
+        content_by_lua_block {
+            local etcd_apisix = require("apisix.core.etcd")
+            local res = {
+                headers = {},
+                body = {
+                    header = {revision = 100},
+                    kvs = {{key = "/test", value = "v", create_revision = "1", 
mod_revision = "2"}},
+                },
+            }
+            local ok, err = etcd_apisix.get_format(res, "/test", false)
+            ngx.say("ok: ", ok ~= nil)
+            ngx.say("err: ", err)
+            ngx.say("revision: ", res.headers["X-Etcd-Index"])
+        }
+    }
+--- request
+GET /t
+--- response_body
+ok: true
+err: nil
+revision: 100

Reply via email to