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
3 changes: 3 additions & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ words:
- secretversion
- TTLs
- vaultname
- DYLD
- dyld
- LDLIBS
languageSettings:
- languageId: go
ignoreRegExpList:
Expand Down
2 changes: 2 additions & 0 deletions cli/azd/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### Bugs Fixed

- [[#8949]](https://github.com/Azure/azure-dev/pull/8949) Dynamic linker/loader control variables (such as `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_AUDIT`, and `DYLD_INSERT_LIBRARIES`) defined in an environment's `.env` file are no longer forwarded into tool subprocesses (docker, npm, python, and others).

### Other Changes

## 1.27.0 (2026-06-30)
Expand Down
31 changes: 24 additions & 7 deletions cli/azd/pkg/environment/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,25 @@ func (e *Environment) DotenvDelete(key string) {
}

// Dotenv returns a copy of the key value pairs from the .env file in the environment.
//
// Dynamic linker/loader control variables (names in the reserved LD_/DYLD_ namespace, e.g.
// LD_PRELOAD or DYLD_INSERT_LIBRARIES) are excluded, because these values flow into the
// environments of the external tools azd runs (directly, and via extensions that read them over
// the gRPC bridge), where the platform dynamic loader would honor them at process launch. They
// remain in the persisted .env file; only what azd hands to callers here is filtered. Matching is
// case-insensitive; names outside that namespace (e.g. LDFLAGS, LDLIBS) are unaffected.
func (e *Environment) Dotenv() map[string]string {
e.mu.RLock()
defer e.mu.RUnlock()
return maps.Clone(e.dotenv)
myDotenv := make(map[string]string, len(e.dotenv))
for k, v := range e.dotenv {
upper := strings.ToUpper(k)
if strings.HasPrefix(upper, "LD_") || strings.HasPrefix(upper, "DYLD_") {
continue
}
myDotenv[k] = v
}
return myDotenv
}

// DotenvSet sets the value of [key] to [value] in the .env file associated with the environment. [Save] should be
Expand Down Expand Up @@ -307,13 +322,15 @@ func (e *Environment) SetServiceProperty(serviceName string, propertyName string
e.DotenvSet(fmt.Sprintf("SERVICE_%s_%s", Key(serviceName), propertyName), value)
}

// Creates a slice of key value pairs, based on the entries in the `.env` file like `KEY=VALUE` that
// can be used to pass into command runner or similar constructs.
// Environ creates a slice of key value pairs, based on the entries in the `.env` file like
// `KEY=VALUE` that can be used to pass into command runner or similar constructs.
//
// The values come from [Environment.Dotenv], so dynamic linker/loader control variables (the
// reserved LD_/DYLD_ namespace) are excluded from the returned environment.
func (e *Environment) Environ() []string {
e.mu.RLock()
defer e.mu.RUnlock()
envVars := []string{}
for k, v := range e.dotenv {
dotenv := e.Dotenv()
envVars := make([]string, 0, len(dotenv))
for k, v := range dotenv {
envVars = append(envVars, fmt.Sprintf("%s=%s", k, v))
}

Expand Down
72 changes: 72 additions & 0 deletions cli/azd/pkg/environment/environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,78 @@ func TestEnvironment_ConcurrentDotenvSet(t *testing.T) {
}
}

// TestEnviron_ExcludesLoaderControlKeys verifies that dynamic linker/loader control variables
// (names in the reserved LD_/DYLD_ namespace) are not carried from the dotenv into the environment
// passed to tool subprocesses, while ordinary values - including similarly named keys such as
// LDFLAGS/LDLIBS that are not in that namespace - are preserved.
func TestEnviron_ExcludesLoaderControlKeys(t *testing.T) {
env := NewWithValues("test-env", map[string]string{
"AZURE_ENV_NAME": "test-env",
"SAFE_VALUE": "keep-me",
// Reserved LD_/DYLD_ loader namespace - must be excluded.
"LD_PRELOAD": "/tmp/a.so",
"LD_LIBRARY_PATH": "/tmp/a",
"LD_AUDIT": "/tmp/audit.so",
"DYLD_INSERT_LIBRARIES": "/tmp/a.dylib",
"DYLD_LIBRARY_PATH": "/tmp/a",
"DYLD_FRAMEWORK_PATH": "/tmp/a",
"DYLD_FALLBACK_LIBRARY_PATH": "/tmp/a",
"DYLD_VERSIONED_LIBRARY_PATH": "/tmp/a",
// Casing must not matter - keys are matched case-insensitively.
"ld_preload": "/tmp/a-lower.so",
// Not in the reserved namespace (no trailing underscore) - must be kept.
"LDFLAGS": "-L/opt/lib",
"LDLIBS": "-lfoo",
})

environ := env.Environ()

// Reserved LD_/DYLD_ namespace entries must be dropped (asserted with literal values so the
// test does not depend on the production filter's own logic).
require.NotContains(t, environ, "LD_PRELOAD=/tmp/a.so")
require.NotContains(t, environ, "LD_LIBRARY_PATH=/tmp/a")
require.NotContains(t, environ, "LD_AUDIT=/tmp/audit.so")
require.NotContains(t, environ, "DYLD_INSERT_LIBRARIES=/tmp/a.dylib")
require.NotContains(t, environ, "DYLD_LIBRARY_PATH=/tmp/a")
require.NotContains(t, environ, "DYLD_FRAMEWORK_PATH=/tmp/a")
require.NotContains(t, environ, "DYLD_FALLBACK_LIBRARY_PATH=/tmp/a")
require.NotContains(t, environ, "DYLD_VERSIONED_LIBRARY_PATH=/tmp/a")
require.NotContains(t, environ, "ld_preload=/tmp/a-lower.so")

// Ordinary values, and similarly named keys outside the reserved namespace, are preserved.
require.Contains(t, environ, "SAFE_VALUE=keep-me")
require.Contains(t, environ, "AZURE_ENV_NAME=test-env")
require.Contains(t, environ, "LDFLAGS=-L/opt/lib")
require.Contains(t, environ, "LDLIBS=-lfoo")
}

// TestDotenv_ExcludesLoaderControlKeys verifies that Dotenv - the copy of the environment's values
// handed to callers, including the subprocess-env paths (azd exec, kubectl, container app bicep
// context) and the gRPC bridge extensions read - excludes the reserved LD_/DYLD_ namespace while
// preserving everything else.
func TestDotenv_ExcludesLoaderControlKeys(t *testing.T) {
env := NewWithValues("test-env", map[string]string{
"AZURE_ENV_NAME": "test-env",
"SAFE_VALUE": "keep-me",
"LD_PRELOAD": "/tmp/a.so",
"LD_LIBRARY_PATH": "/tmp/a",
"DYLD_INSERT_LIBRARIES": "/tmp/a.dylib",
"DYLD_FALLBACK_LIBRARY_PATH": "/tmp/a",
// Case-insensitive match.
"dyld_library_path": "/tmp/a",
// Outside the reserved namespace - preserved.
"LDFLAGS": "-L/opt/lib",
"LDLIBS": "-lfoo",
})

require.Equal(t, map[string]string{
"AZURE_ENV_NAME": "test-env",
"SAFE_VALUE": "keep-me",
"LDFLAGS": "-L/opt/lib",
"LDLIBS": "-lfoo",
}, env.Dotenv())
}

// --- Test 10: Dotenv special characters round-trip ---

func TestDotenvSpecialCharacters_RoundTrip(t *testing.T) {
Expand Down
Loading