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
+        }
+    }

Reply via email to