Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4e3043b
process.env: replace plain object with Node-semantics exotic on POSIX
robobun Jul 26, 2026
cecf118
process.env (Windows proxy): throw on Symbol key/value, match set-tra…
robobun Jul 26, 2026
8023a21
env_var: drop unnecessary #[allow(dead_code)] on reset()
robobun Jul 26, 2026
c445be8
BunObject.rs: drop stale 'Returns the env_loader map entry count' doc…
robobun Jul 26, 2026
47ade95
address review: libcPathForDlopen for musl, skipIf(!cc), afterEach cl…
robobun Jul 26, 2026
fffc7ba
process-env-exotic.test: run freeze/seal probe in subprocess so a fai…
robobun Jul 26, 2026
1a17569
test/preload: skip CI when copying bunEnv into process.env
robobun Jul 26, 2026
808e952
address review: seqlock for string env_var cache, lock map.put/remove…
robobun Jul 26, 2026
210f25b
address review: environ RwLock around setenv/getenv_z, $-prefix read …
robobun Jul 26, 2026
1b2e74d
Bun__ProcessEnv__put: self-truncate NUL + filter empty/= keys; seqloc…
robobun Jul 26, 2026
5143c3e
env_var: store owned copy after runtime setenv (musl frees previous v…
robobun Jul 26, 2026
246257b
env_var::set_owned: lsan-ignore the leaked Box
robobun Jul 26, 2026
9e326a3
Bun__ProcessEnv__delete: clear matching proxy_env_storage slot; fix s…
robobun Jul 26, 2026
7425d37
env_var string cache: always store a leaked owned copy; drop dead res…
robobun Jul 26, 2026
b776d54
getenv_z: copy under ENVIRON_LOCK; worker loader skips load_process r…
robobun Jul 26, 2026
6130d1f
env_var: re-check get_cached() after getenv_z in get_force_reload/pla…
robobun Jul 26, 2026
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
57 changes: 57 additions & 0 deletions src/bun_core/env_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,14 @@ pub(crate) mod kind {

raw_env
}

/// Drop the cached value so the next `get()` re-reads libc.
/// `process.env` writes call setenv() and then this, so the cached
/// pointer into the old environ slot is never dereferenced again.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[inline]
pub(crate) fn reset(&self) {
self.len_value.store(NOT_LOADED_LEN, Ordering::Release);
}
Comment thread
robobun marked this conversation as resolved.
Outdated
}
}

Expand Down Expand Up @@ -448,6 +456,12 @@ pub(crate) mod kind {
);
Some(string_is_truthy)
}

#[inline]
pub(crate) fn reset(&self) {
self.value
.store(StoredType::Unknown as u8, Ordering::Relaxed);
}
}
}

Expand Down Expand Up @@ -619,6 +633,11 @@ pub(crate) mod kind {
}
}
}

#[inline]
pub(crate) fn reset(&self) {
self.value.store(UNKNOWN_SENTINEL, Ordering::Relaxed);
}
}
}
}
Expand Down Expand Up @@ -763,6 +782,13 @@ macro_rules! platform_specific_new {
}
}

/// Drop the cached value so the next `get()` re-reads libc; used by
/// the `process.env` write path after `setenv()`.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[allow(dead_code)]
pub fn reset() {
CACHE.reset();
}
Comment thread
robobun marked this conversation as resolved.

/// Retrieve the value of the environment variable, reloading it from the environment.
/// Fails if the current platform is unsupported.
fn get_force_reload() -> Option<K::ValueType> {
Expand Down Expand Up @@ -951,3 +977,34 @@ macro_rules! new_feature_flag {
};
}
pub(crate) use new_feature_flag;

/// Drop the cached value for the typed accessor whose key matches `name`, so
/// the next `get()` re-reads libc. `process.env` writes call `setenv()` and
/// then this — otherwise `os.homedir()` / `os.tmpdir()` would keep returning
/// the value cached on first read.
///
/// Only the string-kind accessors that are read at runtime (after VM startup)
/// are listed; the `BUN_*` flags are read once on boot and intentionally left
/// cached.
Comment thread
robobun marked this conversation as resolved.
Outdated
Comment thread
robobun marked this conversation as resolved.
pub fn invalidate_for_setenv(name: &[u8]) {
macro_rules! try_var {
($v:ident) => {
if let Some(k) = $v::platform_key() {
if crate::strings::eql(k.as_bytes(), name) {
$v::reset();
return;
}
}
};
}
try_var!(HOME);
try_var!(PATH);
try_var!(USER);
try_var!(TMPDIR);
try_var!(TEMP);
try_var!(TMP);
try_var!(SHELL);
try_var!(XDG_CACHE_HOME);
try_var!(XDG_CONFIG_HOME);
try_var!(XDG_DATA_HOME);
}
31 changes: 21 additions & 10 deletions src/dotenv/env_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -583,19 +583,30 @@ impl Loader {

let environ: &[*const c_char] = bun_sys::environ();
self.map.map.ensure_total_capacity(environ.len())?;
// The kernel accepts duplicate KEY= entries in environ. libc getenv()
// and Node both return the FIRST occurrence, so keep the first and
// skip later duplicates. Indices below `prior_count` were seeded
// before this scan (e.g. `bun test` pre-seeding NODE_ENV) and are
// still overwritten so the process environment retains priority.
Comment thread
robobun marked this conversation as resolved.
let prior_count = self.map.map.count();
for &_env in environ {
// SAFETY: environ entries are NUL-terminated C strings from the OS
let env = unsafe { bun_core::ffi::cstr(_env) }.to_bytes();
if let Some(i) = strings::index_of_char(env, b'=') {
let key = &env[..i as usize];
let value = &env[i as usize + 1..];
if !key.is_empty() {
self.map.put(key, value)?;
}
} else {
if !env.is_empty() {
self.map.put(env, b"")?;
}
// An entry without '=' is malformed per POSIX ("name=value"); libc
// getenv() and Node ignore it.
Comment thread
robobun marked this conversation as resolved.
let Some(i) = strings::index_of_char(env, b'=') else {
continue;
};
let key = &env[..i as usize];
if key.is_empty() {
continue;
}
let value = &env[i as usize + 1..];
let gop = self.map.get_or_put_without_value(key)?;
if !gop.found_existing || gop.index < prior_count {
*gop.value_ptr = HashTableValue {
value: Box::from(value),
};
}
Comment thread
robobun marked this conversation as resolved.
}
Comment thread
robobun marked this conversation as resolved.
self.did_load_process = true;
Expand Down
52 changes: 46 additions & 6 deletions src/js/builtins/ProcessObjectInternals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,9 +490,18 @@
return internalEnv[p];
},
set(_, p, value) {
const k = String(p).toUpperCase();
$assert(typeof p === "string"); // proxy is only string and symbol. the symbol would have thrown by now
// Node's env setter does `ToString` on key and value, which throws on a
// Symbol; JS `String()` special-cases Symbols, so reject explicitly.
Comment thread
robobun marked this conversation as resolved.
if (typeof p === "symbol" || typeof value === "symbol") {
throw new TypeError("Cannot convert a Symbol value to a string");
}
const k = (p as string).toUpperCase();
value = String(value); // If toString() throws, we want to avoid it existing in the envMapList
// Node silently ignores empty names and names containing '=' (the
// assignment succeeds but nothing is stored, matching RealEnvStore::Set).
Comment thread
robobun marked this conversation as resolved.
if (k === "" || k.indexOf("=") !== -1) {
return true;
}
// Track the key for enumeration if it isn't already there. Don't gate on
// `k in internalEnv`: the proxy-related env-var accessors (HTTP_PROXY,
// HTTPS_PROXY, NO_PROXY and lowercase variants) always exist on
Expand Down Expand Up @@ -528,13 +537,44 @@
return typeof p !== "symbol" ? delete internalEnv[k] : false;
},
defineProperty(_, p, attributes) {
const k = String(p).toUpperCase();
$assert(typeof p === "string"); // proxy is only string and symbol. the symbol would have thrown by now
if (typeof p === "symbol") {
throw new TypeError("Cannot convert a Symbol value to a string");
}
Comment thread
robobun marked this conversation as resolved.
const k = (p as string).toUpperCase();
// Node only accepts a fully-permissive data descriptor and routes it
// through the env setter (RealEnvStore::PropertyDefinerCallback).
Comment thread
robobun marked this conversation as resolved.
if ("get" in attributes || "set" in attributes) {
throw $ERR_INVALID_OBJECT_DEFINE_PROPERTY(
"'process.env' does not accept an accessor(getter/setter) descriptor",
);
}
if (
!("value" in attributes) ||
attributes.configurable !== true ||
attributes.writable !== true ||
attributes.enumerable !== true
) {
throw $ERR_INVALID_OBJECT_DEFINE_PROPERTY(
"'process.env' only accepts a configurable, writable, and enumerable data descriptor",
);
}
const value = String(attributes.value);

Check failure on line 561 in src/js/builtins/ProcessObjectInternals.ts

View check run for this annotation

Claude / Claude Code Review

Windows defineProperty trap does not reject Symbol values

The Windows `defineProperty` trap checks `typeof p === "symbol"` for the key but not for `attributes.value`, so `Object.defineProperty(process.env, 'X', {value: Symbol(), writable:true, enumerable:true, configurable:true})` silently stores the string `"Symbol()"` instead of throwing TypeError — diverging from Node and from this PR's own POSIX `JSProcessEnvMap::defineOwnProperty` (which routes through `put()` → `value.isSymbol()`). Add `if (typeof attributes.value === "symbol") throw new TypeErro
Comment thread
robobun marked this conversation as resolved.
if (k === "" || k.indexOf("=") !== -1) {
return true;
}
if (!(k in internalEnv) && !envMapList.includes(p)) {
envMapList.push(p);
}
Comment thread
robobun marked this conversation as resolved.
Outdated
editWindowsEnvVar(k, internalEnv[k]);
return $Object.$defineProperty(internalEnv, k, attributes);
editWindowsEnvVar(k, value);
internalEnv[k] = value;
return true;
},
preventExtensions() {
// Node: Object.freeze/seal/preventExtensions on process.env throw.
return false;
},
isExtensible() {
return true;
},
getOwnPropertyDescriptor(target, p) {
if (typeof p === "string") {
Expand Down
Loading
Loading