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 c808156e9 fix(tracer): prevent stale ctx.tracing crash on HTTPS
keepalive connections (#13232)
c808156e9 is described below
commit c808156e997c77ca672b6880cfc5a31fb2b5734c
Author: Mohammad Izzraff Janius
<[email protected]>
AuthorDate: Thu May 7 01:10:57 2026 -0500
fix(tracer): prevent stale ctx.tracing crash on HTTPS keepalive connections
(#13232)
---
apisix/init.lua | 3 ++
apisix/tracer.lua | 9 ++--
t/lib/test_otel.lua | 63 ++++++++++++++++++++++++++++
t/node/tracer.t | 105 ++++++++++++++++++++++++++++++++++++++++++++++
t/plugin/opentelemetry6.t | 70 +++++++++++++++++++++++++++----
5 files changed, 240 insertions(+), 10 deletions(-)
diff --git a/apisix/init.lua b/apisix/init.lua
index 3b3b92b7b..3e2db0d67 100644
--- a/apisix/init.lua
+++ b/apisix/init.lua
@@ -222,6 +222,7 @@ function _M.ssl_client_hello_phase()
core.log.error("failed to match any SSL certificate by SNI: ", sni)
span:set_status(tracer.status.ERROR, "no matched SSL")
span:finish(ngx_ctx)
+ tracer.release(ngx_ctx)
ngx_exit(-1)
end
@@ -230,6 +231,7 @@ function _M.ssl_client_hello_phase()
core.log.error("failed to set ssl protocols: ", err)
span:set_status(tracer.status.ERROR, "failed set protocols")
span:finish(ngx_ctx)
+ tracer.release(ngx_ctx)
ngx_exit(-1)
end
@@ -237,6 +239,7 @@ function _M.ssl_client_hello_phase()
-- so that we can't get real SNI without recording it in ngx.ctx during
client_hello phase
ngx.ctx.client_hello_sni = sni
span:finish(ngx_ctx)
+ tracer.release(ngx_ctx)
end
diff --git a/apisix/tracer.lua b/apisix/tracer.lua
index ca47730c5..651eecf2e 100644
--- a/apisix/tracer.lua
+++ b/apisix/tracer.lua
@@ -22,6 +22,8 @@ local span_status = require("opentelemetry.trace.span_status")
local local_conf = require("apisix.core.config_local").local_conf()
local ipairs = ipairs
local ngx = ngx
+local rawget = rawget
+local rawset = rawset
local enable_tracing = false
if ngx.config.subsystem == "http" and type(local_conf.apisix.tracing) ==
"boolean" then
@@ -38,11 +40,11 @@ function _M.start(ctx, name, kind)
return noop_span
end
- local tracing = ctx.tracing
+ local tracing = rawget(ctx, "tracing")
if not tracing then
tracing = tablepool.fetch("tracing", 0, 8)
tracing.spans = tablepool.fetch("tracing_spans", 20, 0)
- ctx.tracing = tracing
+ rawset(ctx, "tracing", tracing)
-- create a dummy root span as the invisible parent of all top-level
spans
span.new(ctx, "root", nil)
end
@@ -72,7 +74,7 @@ end
function _M.release(ctx)
- local tracing = ctx.tracing
+ local tracing = rawget(ctx, "tracing")
if not tracing then
return
end
@@ -82,6 +84,7 @@ function _M.release(ctx)
end
tablepool.release("tracing_spans", tracing.spans)
tablepool.release("tracing", tracing)
+ rawset(ctx, "tracing", nil)
end
diff --git a/t/lib/test_otel.lua b/t/lib/test_otel.lua
index 0bbbf1645..fb897cc84 100644
--- a/t/lib/test_otel.lua
+++ b/t/lib/test_otel.lua
@@ -132,4 +132,67 @@ function _M.verify_tree(filepath, expected_tree)
end
+function _M.verify_isolated_traces(filepath, root_name, count, expected_names)
+ local spans_by_id, err = parse_spans(filepath)
+ if not spans_by_id then
+ return false, err
+ end
+
+ local traces = {}
+ for _, span in pairs(spans_by_id) do
+ if not traces[span.traceId] then
+ traces[span.traceId] = {}
+ end
+ table.insert(traces[span.traceId], span.name)
+ end
+
+ local matching = {}
+ for trace_id, names in pairs(traces) do
+ for _, name in ipairs(names) do
+ if name == root_name then
+ table.insert(matching, { id = trace_id, names = names })
+ break
+ end
+ end
+ end
+
+ if #matching ~= count then
+ return false, string.format(
+ "expected %d traces with span '%s', got %d",
+ count, root_name, #matching)
+ end
+
+ local expected_count = {}
+ for _, name in ipairs(expected_names) do
+ expected_count[name] = (expected_count[name] or 0) + 1
+ end
+
+ for _, trace in ipairs(matching) do
+ local actual_count = {}
+ for _, name in ipairs(trace.names) do
+ actual_count[name] = (actual_count[name] or 0) + 1
+ end
+
+ for name, want in pairs(expected_count) do
+ local got = actual_count[name] or 0
+ if got ~= want then
+ return false, string.format(
+ "trace %s: span '%s' expected %d time(s), got %d",
+ trace.id, name, want, got)
+ end
+ end
+
+ for name, got in pairs(actual_count) do
+ if not expected_count[name] then
+ return false, string.format(
+ "trace %s: unexpected span '%s' (%d occurrence(s))",
+ trace.id, name, got)
+ end
+ end
+ end
+
+ return true
+end
+
+
return _M
diff --git a/t/node/tracer.t b/t/node/tracer.t
new file mode 100644
index 000000000..061c94b6c
--- /dev/null
+++ b/t/node/tracer.t
@@ -0,0 +1,105 @@
+#
+# 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);
+log_level('debug');
+no_root_location();
+no_shuffle();
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if (!$block->extra_yaml_config) {
+ my $extra_yaml_config = <<_EOC_;
+apisix:
+ tracing: true
+_EOC_
+ $block->set_value("extra_yaml_config", $extra_yaml_config);
+ }
+
+ if (!$block->request) {
+ $block->set_value("request", "GET /t");
+ }
+
+ if (!defined $block->response_body) {
+ $block->set_value("response_body", "passed\n");
+ }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: set SSL cert for test.com
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+ local ssl_cert = t.read_file("t/certs/apisix.crt")
+ local ssl_key = t.read_file("t/certs/apisix.key")
+ local core = require("apisix.core")
+ local data = {cert = ssl_cert, key = ssl_key, sni = "test.com"}
+ local code, body = t.test('/apisix/admin/ssls/1',
+ ngx.HTTP_PUT,
+ core.json.encode(data),
+ [[{
+ "value": {
+ "sni": "test.com"
+ },
+ "key": "/apisix/ssls/1"
+ }]]
+ )
+ ngx.status = code
+ ngx.say(body)
+ }
+ }
+
+
+
+=== TEST 2: set route
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/opentracing"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+
+
+
+=== TEST 3: consecutive HTTPS keepalive requests do not crash when tracing is
enabled
+--- exec
+curl -s -k https://test.com:1994/opentracing https://test.com:1994/opentracing
+--- response_body
+opentracing
+opentracing
diff --git a/t/plugin/opentelemetry6.t b/t/plugin/opentelemetry6.t
index 7504af34a..db3590673 100644
--- a/t/plugin/opentelemetry6.t
+++ b/t/plugin/opentelemetry6.t
@@ -218,13 +218,6 @@ opentracing
["http.status_code"] = "200",
},
children = {
- {
- name = "ssl_client_hello_phase",
- kind = 2,
- children = {
- { name = "sni_radixtree_match", kind = 1 },
- }
- },
{
name = "apisix.phase.access",
kind = 2,
@@ -248,3 +241,66 @@ opentracing
end
}
}
+
+
+
+=== TEST 7: clear file
+--- exec
+echo '' > ci/pod/otelcol-contrib/data-otlp.json
+--- response_body eval
+qr//
+
+
+
+=== TEST 8: trigger two HTTP/2 requests on the same TLS connection
+--- init_by_lua_block
+ require "resty.core"
+ apisix = require("apisix")
+ core = require("apisix.core")
+ apisix.http_init()
+
+ local utils = require("apisix.core.utils")
+ utils.dns_parse = function (domain)
+ if domain == "test1.com" then
+ return {address = "127.0.0.2"}
+ end
+ error("unknown domain: " .. domain)
+ end
+--- exec
+curl -sk --http2 --resolve "test.com:1994:127.0.0.1"
https://test.com:1994/opentracing https://test.com:1994/opentracing
+--- wait: 5
+--- response_body
+opentracing
+opentracing
+
+
+
+=== TEST 9: verify each HTTP/2 stream has its own isolated span set
+--- config
+ location /t {
+ content_by_lua_block {
+ local otel = require("lib.test_otel")
+
+ local ok, err = otel.verify_isolated_traces(
+ "ci/pod/otelcol-contrib/data-otlp.json",
+ "GET /opentracing",
+ 2,
+ {
+ "GET /opentracing",
+ "apisix.phase.access",
+ "sni_radixtree_match",
+ "http_router_match",
+ "resolve_dns",
+ "apisix.phase.header_filter",
+ "apisix.phase.body_filter",
+ "apisix.phase.log.plugins.opentelemetry",
+ }
+ )
+
+ if not ok then
+ ngx.say("FAIL:\n" .. err)
+ else
+ ngx.say("passed")
+ end
+ }
+ }