Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion apisix/core/env.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions apisix/discovery/kubernetes/core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions apisix/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions t/core/env.t
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading