diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 0c2d440ed42..2a94b756055 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -156,6 +156,9 @@ words: - secretversion - TTLs - vaultname + - DYLD + - dyld + - LDLIBS languageSettings: - languageId: go ignoreRegExpList: diff --git a/cli/azd/CHANGELOG.md b/cli/azd/CHANGELOG.md index 9f15a429fbc..a28ffab1296 100644 --- a/cli/azd/CHANGELOG.md +++ b/cli/azd/CHANGELOG.md @@ -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) diff --git a/cli/azd/pkg/environment/environment.go b/cli/azd/pkg/environment/environment.go index 67774924e28..7e0ba86c5e9 100644 --- a/cli/azd/pkg/environment/environment.go +++ b/cli/azd/pkg/environment/environment.go @@ -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 @@ -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)) } diff --git a/cli/azd/pkg/environment/environment_test.go b/cli/azd/pkg/environment/environment_test.go index baf6dc8d1f2..19ebf335492 100644 --- a/cli/azd/pkg/environment/environment_test.go +++ b/cli/azd/pkg/environment/environment_test.go @@ -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) {