This is an automated email from the ASF dual-hosted git repository.
AlinsRan 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 43be2b97e fix(env): resolve os.getenv prefix collision for env
directives (#13595)
43be2b97e is described below
commit 43be2b97ec582960b93d7ecf592bd6f81e73668a
Author: AlinsRan <[email protected]>
AuthorDate: Mon Jun 29 12:38:54 2026 +0800
fix(env): resolve os.getenv prefix collision for env directives (#13595)
---
apisix/core/env.lua | 37 +++++++++++++++-
apisix/discovery/kubernetes/core.lua | 7 ++-
apisix/init.lua | 12 +++++
t/core/env.t | 86 ++++++++++++++++++++++++++++++++++++
4 files changed, 139 insertions(+), 3 deletions(-)
diff --git a/apisix/core/env.lua b/apisix/core/env.lua
index 6a57a70ed..0ddeee120 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 1ba74408d..aaf0142a8 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 311ba4869..32c9dfa08 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()
@@ -1261,6 +1267,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
diff --git a/t/core/env.t b/t/core/env.t
index 2e14a4397..4168eae4c 100644
--- a/t/core/env.t
+++ b/t/core/env.t
@@ -179,3 +179,89 @@ 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;
+--- 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 {
+ 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