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