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..d4547d2ff570 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() @@ -1244,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 diff --git a/t/core/env.t b/t/core/env.t index 2e14a4397e73..4168eae4cb39 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