Skip to content

Commit 43be2b9

Browse files
authored
fix(env): resolve os.getenv prefix collision for env directives (#13595)
1 parent 7f5638b commit 43be2b9

4 files changed

Lines changed: 139 additions & 3 deletions

File tree

apisix/core/env.lua

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,26 @@ ffi.cdef [[
4141
]]
4242

4343

44+
-- Build an exact-keyed table from the process environment (`environ`).
45+
--
46+
-- This is intentionally used instead of `os.getenv` to sidestep a bug in
47+
-- lua-resty-core's `os.getenv` shim that is active before any request is
48+
-- being served (init / init_worker phases). That shim relies on
49+
-- `ngx_http_lua_ffi_get_conf_env`, which matches an `env NAME=VALUE;`
50+
-- directive entry against the queried name with a prefix-only comparison
51+
-- (`ngx_strncmp(name, var.data, var.len)`) and does not require the queried
52+
-- name to end at `var.len`. As a result, when two `env` directives share a
53+
-- common prefix (e.g. `KUBERNETES_CLIENT_TOKEN` and
54+
-- `KUBERNETES_CLIENT_TOKEN_FILE`), the shorter declared name shadows the
55+
-- longer one. Reading `environ` directly and keying by the substring before
56+
-- the first `=` avoids the collision entirely. See apache/apisix#13055.
57+
--
58+
-- Note on phases: nginx applies `env NAME=VALUE;` directives to the real
59+
-- `environ` in `ngx_set_environment`, which runs at worker process start
60+
-- (before `init_worker_by_lua`). Therefore `init()` must be called again in
61+
-- the worker init phase so that directive-assigned values are captured; at
62+
-- the `init_by_lua` phase `environ` only contains variables inherited from
63+
-- the OS, not the directive-assigned ones.
4464
function _M.init()
4565
local e = ffi.C.environ
4666
if not e then
@@ -61,6 +81,21 @@ function _M.init()
6181
end
6282

6383

84+
-- Look up an environment variable by exact name.
85+
--
86+
-- Prefer the snapshot built by `init()` (immune to the prefix-collision bug
87+
-- described above) and only fall back to `os.getenv` for variables that were
88+
-- set dynamically after startup (e.g. via `core.os.setenv`).
89+
function _M.get(name)
90+
local val = apisix_env_vars[name]
91+
if val ~= nil then
92+
return val
93+
end
94+
95+
return os.getenv(name)
96+
end
97+
98+
6499
local function parse_env_uri(env_uri)
65100
-- Avoid the error caused by has_prefix to cause a crash.
66101
if type(env_uri) ~= "string" then
@@ -93,7 +128,7 @@ function _M.fetch_by_uri(env_uri)
93128
return nil, err
94129
end
95130

96-
local main_value = apisix_env_vars[opts.key] or os.getenv(opts.key)
131+
local main_value = _M.get(opts.key)
97132
if main_value and opts.sub_key ~= "" then
98133
local vt, err = json.decode(main_value)
99134
if not vt then

apisix/discovery/kubernetes/core.lua

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ local unpack = unpack
2626
local string = string
2727
local tonumber = tonumber
2828
local tostring = tostring
29-
local os = os
3029
local pcall = pcall
3130
local setmetatable = setmetatable
3231

@@ -68,7 +67,11 @@ function _M.read_env(key)
6867
local last = string.byte(key, #key)
6968
if last == string.byte('}') then
7069
local env = string.sub(key, 3, #key - 1)
71-
local value = os.getenv(env)
70+
-- Use core.env.get for exact-match lookup, avoiding the
71+
-- os.getenv prefix-collision bug in the worker init phase
72+
-- (e.g. KUBERNETES_CLIENT_TOKEN vs KUBERNETES_CLIENT_TOKEN_FILE).
73+
-- See apache/apisix#13055.
74+
local value = core.env.get(env)
7275
if not value then
7376
return nil, "not found environment variable " .. env
7477
end

apisix/init.lua

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ function _M.http_init_worker()
120120
-- for testing only
121121
core.log.info("random test in [1, 10000]: ", math.random(1, 10000))
122122

123+
-- Re-read the environment in the worker phase: nginx applies
124+
-- `env NAME=VALUE;` directives to `environ` at worker start (in
125+
-- ngx_set_environment), after the init phase. This rebuild ensures
126+
-- directive-assigned values are captured with exact keys. See #13055.
127+
core.env.init()
128+
123129
require("apisix.events").init_worker()
124130

125131
core.lrucache.init_worker()
@@ -1261,6 +1267,12 @@ function _M.stream_init_worker()
12611267
-- for testing only
12621268
core.log.info("random stream test in [1, 10000]: ", math.random(1, 10000))
12631269

1270+
-- The stream subsystem runs in its own Lua VM, so the env snapshot built in
1271+
-- http_init_worker is not visible here. Rebuild it before any consumer (e.g.
1272+
-- kubernetes discovery's read_env) runs, otherwise core.env.get falls back to
1273+
-- the buggy os.getenv shim. See #13055.
1274+
core.env.init()
1275+
12641276
core.lrucache.init_worker()
12651277

12661278
if core.config.init_worker then

t/core/env.t

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,89 @@ env ngx_env=apisix-nice;
179179
GET /t
180180
--- response_body
181181
apisix-nice
182+
183+
184+
185+
=== TEST 10: env directives sharing a common prefix must not collide (#13055)
186+
--- main_config
187+
env KUBERNETES_CLIENT_TOKEN=some-token;
188+
env KUBERNETES_CLIENT_TOKEN_FILE=/path/to/token;
189+
--- config
190+
location /t {
191+
content_by_lua_block {
192+
local env = require("apisix.core.env")
193+
ngx.say(env.fetch_by_uri("$ENV://KUBERNETES_CLIENT_TOKEN"))
194+
ngx.say(env.fetch_by_uri("$ENV://KUBERNETES_CLIENT_TOKEN_FILE"))
195+
}
196+
}
197+
--- request
198+
GET /t
199+
--- response_body
200+
some-token
201+
/path/to/token
202+
203+
204+
205+
=== TEST 11: longer-named directive declared first must not collide (#13055)
206+
--- main_config
207+
env KUBERNETES_CLIENT_TOKEN_FILE=/path/to/token;
208+
env KUBERNETES_CLIENT_TOKEN=some-token;
209+
--- config
210+
location /t {
211+
content_by_lua_block {
212+
local env = require("apisix.core.env")
213+
ngx.say(env.fetch_by_uri("$ENV://KUBERNETES_CLIENT_TOKEN"))
214+
ngx.say(env.fetch_by_uri("$ENV://KUBERNETES_CLIENT_TOKEN_FILE"))
215+
}
216+
}
217+
--- request
218+
GET /t
219+
--- response_body
220+
some-token
221+
/path/to/token
222+
223+
224+
225+
=== TEST 12: core.env.get resolves prefix-colliding directives exactly (#13055)
226+
--- main_config
227+
env COLLIDE_PREFIX=prefix-value;
228+
env COLLIDE_PREFIX_LONGER=longer-value;
229+
--- config
230+
location /t {
231+
content_by_lua_block {
232+
local env = require("apisix.core.env")
233+
ngx.say(env.get("COLLIDE_PREFIX"))
234+
ngx.say(env.get("COLLIDE_PREFIX_LONGER"))
235+
}
236+
}
237+
--- request
238+
GET /t
239+
--- response_body
240+
prefix-value
241+
longer-value
242+
243+
244+
245+
=== TEST 13: resolve prefix-colliding directives during worker init phase (#13055)
246+
--- main_config
247+
env INIT_COLLIDE_TOKEN=some-token;
248+
env INIT_COLLIDE_TOKEN_FILE=/path/to/token;
249+
--- extra_init_worker_by_lua
250+
local env = require("apisix.core.env")
251+
package.loaded["test_init_env"] = {
252+
token = env.get("INIT_COLLIDE_TOKEN"),
253+
token_file = env.get("INIT_COLLIDE_TOKEN_FILE"),
254+
}
255+
--- config
256+
location /t {
257+
content_by_lua_block {
258+
local result = package.loaded["test_init_env"]
259+
ngx.say(result.token)
260+
ngx.say(result.token_file)
261+
}
262+
}
263+
--- request
264+
GET /t
265+
--- response_body
266+
some-token
267+
/path/to/token

0 commit comments

Comments
 (0)