From 0fcc4426b2118f20f25940e407ed5fcc4f01d7a6 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Tue, 23 Jun 2026 04:44:05 +0800 Subject: [PATCH 1/3] fix(env): resolve os.getenv prefix collision for env directives When two `env NAME=VALUE;` directives share a common prefix (e.g. KUBERNETES_CLIENT_TOKEN and KUBERNETES_CLIENT_TOKEN_FILE), looking up the longer-named one in the init/init_worker phase returns the shorter one's value. The root cause is lua-resty-core's os.getenv shim, which delegates to ngx_http_lua_ffi_get_conf_env; that function matches with a prefix-only ngx_strncmp and does not require the queried name to end at the configured entry's length. Resolve env variables from `environ` directly with exact keys instead of the shim. nginx applies env directives to `environ` in ngx_set_environment at worker start, so core.env.init() is also invoked in the worker init phase to capture directive-assigned values. The kubernetes discovery and $ENV:// resolution now go through core.env.get for an exact-match lookup. Fixes #13055 --- apisix/core/env.lua | 37 +++++++++++- apisix/discovery/kubernetes/core.lua | 7 ++- apisix/init.lua | 6 ++ t/core/env.t | 89 ++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 3 deletions(-) diff --git a/apisix/core/env.lua b/apisix/core/env.lua index 6a57a70edd15..0ddeee1203e4 100644 --- a/apisix/core/env.lua +++ b/apisix/core/env.lua @@ -41,6 +41,26 @@ ffi.cdef [[ ]] +-- Build an exact-keyed table from the process environment (`environ`). +-- +-- This is intentionally used instead of `os.getenv` to sidestep a bug in +-- lua-resty-core's `os.getenv` shim that is active before any request is +-- being served (init / init_worker phases). That shim relies on +-- `ngx_http_lua_ffi_get_conf_env`, which matches an `env NAME=VALUE;` +-- directive entry against the queried name with a prefix-only comparison +-- (`ngx_strncmp(name, var.data, var.len)`) and does not require the queried +-- name to end at `var.len`. As a result, when two `env` directives share a +-- common prefix (e.g. `KUBERNETES_CLIENT_TOKEN` and +-- `KUBERNETES_CLIENT_TOKEN_FILE`), the shorter declared name shadows the +-- longer one. Reading `environ` directly and keying by the substring before +-- the first `=` avoids the collision entirely. See apache/apisix#13055. +-- +-- Note on phases: nginx applies `env NAME=VALUE;` directives to the real +-- `environ` in `ngx_set_environment`, which runs at worker process start +-- (before `init_worker_by_lua`). Therefore `init()` must be called again in +-- the worker init phase so that directive-assigned values are captured; at +-- the `init_by_lua` phase `environ` only contains variables inherited from +-- the OS, not the directive-assigned ones. function _M.init() local e = ffi.C.environ if not e then @@ -61,6 +81,21 @@ function _M.init() end +-- Look up an environment variable by exact name. +-- +-- Prefer the snapshot built by `init()` (immune to the prefix-collision bug +-- described above) and only fall back to `os.getenv` for variables that were +-- set dynamically after startup (e.g. via `core.os.setenv`). +function _M.get(name) + local val = apisix_env_vars[name] + if val ~= nil then + return val + end + + return os.getenv(name) +end + + local function parse_env_uri(env_uri) -- Avoid the error caused by has_prefix to cause a crash. if type(env_uri) ~= "string" then @@ -93,7 +128,7 @@ function _M.fetch_by_uri(env_uri) return nil, err end - local main_value = apisix_env_vars[opts.key] or os.getenv(opts.key) + local main_value = _M.get(opts.key) if main_value and opts.sub_key ~= "" then local vt, err = json.decode(main_value) if not vt then diff --git a/apisix/discovery/kubernetes/core.lua b/apisix/discovery/kubernetes/core.lua index 1ba74408d9e2..aaf0142a894d 100644 --- a/apisix/discovery/kubernetes/core.lua +++ b/apisix/discovery/kubernetes/core.lua @@ -26,7 +26,6 @@ local unpack = unpack local string = string local tonumber = tonumber local tostring = tostring -local os = os local pcall = pcall local setmetatable = setmetatable @@ -68,7 +67,11 @@ function _M.read_env(key) local last = string.byte(key, #key) if last == string.byte('}') then local env = string.sub(key, 3, #key - 1) - local value = os.getenv(env) + -- Use core.env.get for exact-match lookup, avoiding the + -- os.getenv prefix-collision bug in the worker init phase + -- (e.g. KUBERNETES_CLIENT_TOKEN vs KUBERNETES_CLIENT_TOKEN_FILE). + -- See apache/apisix#13055. + local value = core.env.get(env) if not value then return nil, "not found environment variable " .. env end diff --git a/apisix/init.lua b/apisix/init.lua index 01838da5b0f4..2939a5849675 100644 --- a/apisix/init.lua +++ b/apisix/init.lua @@ -120,6 +120,12 @@ function _M.http_init_worker() -- for testing only core.log.info("random test in [1, 10000]: ", math.random(1, 10000)) + -- Re-read the environment in the worker phase: nginx applies + -- `env NAME=VALUE;` directives to `environ` at worker start (in + -- ngx_set_environment), after the init phase. This rebuild ensures + -- directive-assigned values are captured with exact keys. See #13055. + core.env.init() + require("apisix.events").init_worker() core.lrucache.init_worker() diff --git a/t/core/env.t b/t/core/env.t index 2e14a4397e73..63bddb4f1de1 100644 --- a/t/core/env.t +++ b/t/core/env.t @@ -179,3 +179,92 @@ env ngx_env=apisix-nice; GET /t --- response_body apisix-nice + + + +=== TEST 10: env directives sharing a common prefix must not collide (#13055) +--- main_config +env KUBERNETES_CLIENT_TOKEN=some-token; +env KUBERNETES_CLIENT_TOKEN_FILE=/path/to/token; +--- config + location /t { + content_by_lua_block { + local env = require("apisix.core.env") + ngx.say(env.fetch_by_uri("$ENV://KUBERNETES_CLIENT_TOKEN")) + ngx.say(env.fetch_by_uri("$ENV://KUBERNETES_CLIENT_TOKEN_FILE")) + } + } +--- request +GET /t +--- response_body +some-token +/path/to/token + + + +=== TEST 11: longer-named directive declared first must not collide (#13055) +--- main_config +env KUBERNETES_CLIENT_TOKEN_FILE=/path/to/token; +env KUBERNETES_CLIENT_TOKEN=some-token; +--- config + location /t { + content_by_lua_block { + local env = require("apisix.core.env") + ngx.say(env.fetch_by_uri("$ENV://KUBERNETES_CLIENT_TOKEN")) + ngx.say(env.fetch_by_uri("$ENV://KUBERNETES_CLIENT_TOKEN_FILE")) + } + } +--- request +GET /t +--- response_body +some-token +/path/to/token + + + +=== TEST 12: core.env.get resolves prefix-colliding directives exactly (#13055) +--- main_config +env COLLIDE_PREFIX=prefix-value; +env COLLIDE_PREFIX_LONGER=longer-value; +--- config + location /t { + content_by_lua_block { + local env = require("apisix.core.env") + ngx.say(env.get("COLLIDE_PREFIX")) + ngx.say(env.get("COLLIDE_PREFIX_LONGER")) + } + } +--- request +GET /t +--- response_body +prefix-value +longer-value + + + +=== TEST 13: resolve prefix-colliding directives during worker init phase (#13055) +--- main_config +env INIT_COLLIDE_TOKEN=some-token; +env INIT_COLLIDE_TOKEN_FILE=/path/to/token; +--- http_config + init_worker_by_lua_block { + local env = require("apisix.core.env") + env.init() + package.loaded["test_init_env"] = { + token = env.get("INIT_COLLIDE_TOKEN"), + token_file = env.get("INIT_COLLIDE_TOKEN_FILE"), + } + } +--- config + location /t { + content_by_lua_block { + local result = package.loaded["test_init_env"] + ngx.say(result.token) + ngx.say(result.token_file) + } + } +--- request +GET /t +--- response_body +some-token +/path/to/token From 58bf15b073f039d9c543006478c358110cfb7c87 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Tue, 23 Jun 2026 05:33:26 +0800 Subject: [PATCH 2/3] test: fix duplicate init_worker_by_lua_block in env.t TEST 13 TEST 13 supplied its own init_worker_by_lua_block via --- http_config, which collides with the one the test framework always emits inside the http{} block, causing nginx to fail with 'init_worker_by_lua_block is duplicate'. Use the framework's --- extra_init_worker_by_lua section, which injects into the existing block (after http_init_worker, where the env snapshot is already rebuilt for the worker phase). --- t/core/env.t | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/t/core/env.t b/t/core/env.t index 63bddb4f1de1..4168eae4cb39 100644 --- a/t/core/env.t +++ b/t/core/env.t @@ -246,14 +246,11 @@ longer-value --- main_config env INIT_COLLIDE_TOKEN=some-token; env INIT_COLLIDE_TOKEN_FILE=/path/to/token; ---- http_config - init_worker_by_lua_block { - local env = require("apisix.core.env") - env.init() - package.loaded["test_init_env"] = { - token = env.get("INIT_COLLIDE_TOKEN"), - token_file = env.get("INIT_COLLIDE_TOKEN_FILE"), - } +--- extra_init_worker_by_lua + local env = require("apisix.core.env") + package.loaded["test_init_env"] = { + token = env.get("INIT_COLLIDE_TOKEN"), + token_file = env.get("INIT_COLLIDE_TOKEN_FILE"), } --- config location /t { From c7ba4f1332eee6cee525afe04f0255dca1ae99f8 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Tue, 23 Jun 2026 10:36:07 +0800 Subject: [PATCH 3/3] fix(env): rebuild env snapshot in stream_init_worker too The stream subsystem runs in a separate Lua VM, so the snapshot built in http_init_worker is absent there. kubernetes discovery's read_env runs in stream_init_worker -> discovery.init_worker() during the init_worker phase (get_request()==nil), so without re-init core.env.get falls back to the buggy os.getenv shim and the #13055 prefix collision is not fixed under the stream subsystem. --- apisix/init.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apisix/init.lua b/apisix/init.lua index 2939a5849675..d4547d2ff570 100644 --- a/apisix/init.lua +++ b/apisix/init.lua @@ -1250,6 +1250,12 @@ function _M.stream_init_worker() -- for testing only core.log.info("random stream test in [1, 10000]: ", math.random(1, 10000)) + -- The stream subsystem runs in its own Lua VM, so the env snapshot built in + -- http_init_worker is not visible here. Rebuild it before any consumer (e.g. + -- kubernetes discovery's read_env) runs, otherwise core.env.get falls back to + -- the buggy os.getenv shim. See #13055. + core.env.init() + core.lrucache.init_worker() if core.config.init_worker then