From d23fb1ced032e17af15ea04a9ddaedf211023375 Mon Sep 17 00:00:00 2001 From: hemarina Date: Thu, 2 Jul 2026 14:25:07 -0700 Subject: [PATCH 1/6] Filter dynamic loader control variables from environment propagation azd propagated the entire azd-environment dotenv (.azure//.env) into tool subprocesses (docker build, npm, python, maven, go, dotnet, bicep) without filtering dynamic linker/loader control variables. A dotenv entry such as LD_PRELOAD or DYLD_INSERT_LIBRARIES would then be honored by the dynamic loader when the subprocess launches. Strip these loader/injection control variables at the single source, Environment.Environ(), so every subprocess path is covered by one centralized denylist. Add a regression test asserting Environ() drops the loader keys (case-insensitively) while preserving safe values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/CHANGELOG.md | 2 ++ cli/azd/pkg/environment/environment.go | 21 ++++++++++++++ cli/azd/pkg/environment/environment_test.go | 32 +++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/cli/azd/CHANGELOG.md b/cli/azd/CHANGELOG.md index 9f15a429fbc..e4572a010de 100644 --- a/cli/azd/CHANGELOG.md +++ b/cli/azd/CHANGELOG.md @@ -8,6 +8,8 @@ ### Bugs Fixed +- [[#PRNUM]](https://github.com/Azure/azure-dev/pull/PRNUM) 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..c48b51b7f7d 100644 --- a/cli/azd/pkg/environment/environment.go +++ b/cli/azd/pkg/environment/environment.go @@ -307,13 +307,34 @@ func (e *Environment) SetServiceProperty(serviceName string, propertyName string e.DotenvSet(fmt.Sprintf("SERVICE_%s_%s", Key(serviceName), propertyName), value) } +// unsafeEnvironKeys are dynamic-loader and injection control variables that must never be +// propagated from an azd environment's dotenv into tool subprocesses (docker, npm, python, +// maven, go, dotnet, bicep, az, etc.). A dotenv value for any of these keys would let +// attacker-controlled code be loaded into a subprocess at launch time. These variables have no +// legitimate reason to live in an azd dotenv, so they are stripped at the single source that +// feeds every subprocess environment. +var unsafeEnvironKeys = map[string]struct{}{ + "LD_PRELOAD": {}, + "LD_LIBRARY_PATH": {}, + "LD_AUDIT": {}, + "DYLD_INSERT_LIBRARIES": {}, + "DYLD_LIBRARY_PATH": {}, + "DYLD_FRAMEWORK_PATH": {}, +} + // 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. +// +// Dynamic-loader and injection control variables (see [unsafeEnvironKeys]) are omitted so they are +// never propagated from the dotenv into tool subprocesses. func (e *Environment) Environ() []string { e.mu.RLock() defer e.mu.RUnlock() envVars := []string{} for k, v := range e.dotenv { + if _, unsafe := unsafeEnvironKeys[strings.ToUpper(k)]; unsafe { + continue + } 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..3454c7fc386 100644 --- a/cli/azd/pkg/environment/environment_test.go +++ b/cli/azd/pkg/environment/environment_test.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "sync" "testing" @@ -346,6 +347,37 @@ func TestEnvironment_ConcurrentDotenvSet(t *testing.T) { } } +// TestEnviron_StripsUnsafeLoaderKeys verifies that dynamic-loader and injection control +// variables are never propagated from the dotenv into subprocess environments. A dotenv value +// for one of these keys (e.g. injected by a malicious template or a poisoned shared env store) +// would otherwise load attacker-controlled code into tool subprocesses at launch time. +func TestEnviron_StripsUnsafeLoaderKeys(t *testing.T) { + env := NewWithValues("test-env", map[string]string{ + "AZURE_ENV_NAME": "test-env", + "SAFE_VALUE": "keep-me", + "LD_PRELOAD": "/tmp/evil.so", + "LD_LIBRARY_PATH": "/tmp/evil", + "LD_AUDIT": "/tmp/audit.so", + "DYLD_INSERT_LIBRARIES": "/tmp/evil.dylib", + "DYLD_LIBRARY_PATH": "/tmp/evil", + "DYLD_FRAMEWORK_PATH": "/tmp/evil", + // Casing must not matter - dotenv keys are matched case-insensitively. + "ld_preload": "/tmp/evil-lower.so", + }) + + environ := env.Environ() + + for _, entry := range environ { + key, _, _ := strings.Cut(entry, "=") + if _, unsafe := unsafeEnvironKeys[strings.ToUpper(key)]; unsafe { + t.Errorf("Environ() must not contain loader/injection key %q, got entry %q", key, entry) + } + } + + require.Contains(t, environ, "SAFE_VALUE=keep-me") + require.Contains(t, environ, "AZURE_ENV_NAME=test-env") +} + // --- Test 10: Dotenv special characters round-trip --- func TestDotenvSpecialCharacters_RoundTrip(t *testing.T) { From a228628227b650536559578e8f801ec88a6bbfa7 Mon Sep 17 00:00:00 2001 From: hemarina Date: Thu, 2 Jul 2026 14:26:25 -0700 Subject: [PATCH 2/6] Update CHANGELOG with PR number Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/CHANGELOG.md b/cli/azd/CHANGELOG.md index e4572a010de..a28ffab1296 100644 --- a/cli/azd/CHANGELOG.md +++ b/cli/azd/CHANGELOG.md @@ -8,7 +8,7 @@ ### Bugs Fixed -- [[#PRNUM]](https://github.com/Azure/azure-dev/pull/PRNUM) 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). +- [[#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 From d9a97fb500a3d0b24b46dcecc4360ab1aa04ade1 Mon Sep 17 00:00:00 2001 From: hemarina Date: Thu, 2 Jul 2026 16:21:29 -0700 Subject: [PATCH 3/6] use prefix --- cli/azd/pkg/environment/environment.go | 26 +++------ cli/azd/pkg/environment/environment_test.go | 59 +++++++++++++-------- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/cli/azd/pkg/environment/environment.go b/cli/azd/pkg/environment/environment.go index c48b51b7f7d..520ca93366c 100644 --- a/cli/azd/pkg/environment/environment.go +++ b/cli/azd/pkg/environment/environment.go @@ -307,32 +307,22 @@ func (e *Environment) SetServiceProperty(serviceName string, propertyName string e.DotenvSet(fmt.Sprintf("SERVICE_%s_%s", Key(serviceName), propertyName), value) } -// unsafeEnvironKeys are dynamic-loader and injection control variables that must never be -// propagated from an azd environment's dotenv into tool subprocesses (docker, npm, python, -// maven, go, dotnet, bicep, az, etc.). A dotenv value for any of these keys would let -// attacker-controlled code be loaded into a subprocess at launch time. These variables have no -// legitimate reason to live in an azd dotenv, so they are stripped at the single source that -// feeds every subprocess environment. -var unsafeEnvironKeys = map[string]struct{}{ - "LD_PRELOAD": {}, - "LD_LIBRARY_PATH": {}, - "LD_AUDIT": {}, - "DYLD_INSERT_LIBRARIES": {}, - "DYLD_LIBRARY_PATH": {}, - "DYLD_FRAMEWORK_PATH": {}, -} - // 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. // -// Dynamic-loader and injection control variables (see [unsafeEnvironKeys]) are omitted so they are -// never propagated from the dotenv into tool subprocesses. +// Dynamic linker/loader control variables (the reserved LD_/DYLD_ namespace) are omitted so they +// are not carried from the dotenv into the environment of tools azd runs. func (e *Environment) Environ() []string { e.mu.RLock() defer e.mu.RUnlock() envVars := []string{} for k, v := range e.dotenv { - if _, unsafe := unsafeEnvironKeys[strings.ToUpper(k)]; unsafe { + // Skip dynamic linker/loader control variables (the reserved LD_/DYLD_ namespace): they + // change how a process resolves and loads shared libraries at launch, so they must not be + // carried from the dotenv into tool subprocess environments. Matched case-insensitively; + // LDFLAGS/LDLIBS (no underscore) are intentionally unaffected. + upper := strings.ToUpper(k) + if strings.HasPrefix(upper, "LD_") || strings.HasPrefix(upper, "DYLD_") { continue } 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 3454c7fc386..f2d7bd7e87c 100644 --- a/cli/azd/pkg/environment/environment_test.go +++ b/cli/azd/pkg/environment/environment_test.go @@ -9,7 +9,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "sync" "testing" @@ -347,35 +346,49 @@ func TestEnvironment_ConcurrentDotenvSet(t *testing.T) { } } -// TestEnviron_StripsUnsafeLoaderKeys verifies that dynamic-loader and injection control -// variables are never propagated from the dotenv into subprocess environments. A dotenv value -// for one of these keys (e.g. injected by a malicious template or a poisoned shared env store) -// would otherwise load attacker-controlled code into tool subprocesses at launch time. -func TestEnviron_StripsUnsafeLoaderKeys(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", - "LD_PRELOAD": "/tmp/evil.so", - "LD_LIBRARY_PATH": "/tmp/evil", - "LD_AUDIT": "/tmp/audit.so", - "DYLD_INSERT_LIBRARIES": "/tmp/evil.dylib", - "DYLD_LIBRARY_PATH": "/tmp/evil", - "DYLD_FRAMEWORK_PATH": "/tmp/evil", - // Casing must not matter - dotenv keys are matched case-insensitively. - "ld_preload": "/tmp/evil-lower.so", + "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() - for _, entry := range environ { - key, _, _ := strings.Cut(entry, "=") - if _, unsafe := unsafeEnvironKeys[strings.ToUpper(key)]; unsafe { - t.Errorf("Environ() must not contain loader/injection key %q, got entry %q", key, entry) - } - } - + // 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") } // --- Test 10: Dotenv special characters round-trip --- From 4787a8ec965fa41f1d26ae4e6e7e9fa8cd8d719a Mon Sep 17 00:00:00 2001 From: hemarina Date: Thu, 2 Jul 2026 16:36:49 -0700 Subject: [PATCH 4/6] address feedback, close gap --- cli/azd/cmd/exec.go | 2 +- cli/azd/pkg/environment/environment.go | 34 +++++++++++++++---- cli/azd/pkg/environment/environment_test.go | 26 ++++++++++++++ cli/azd/pkg/project/service_target_aks.go | 4 +-- .../service_target_dotnet_containerapp.go | 2 +- 5 files changed, 58 insertions(+), 10 deletions(-) diff --git a/cli/azd/cmd/exec.go b/cli/azd/cmd/exec.go index 8918319fd88..2645f5cec5c 100644 --- a/cli/azd/cmd/exec.go +++ b/cli/azd/cmd/exec.go @@ -118,7 +118,7 @@ func newExecAction( func (a *execAction) buildChildEnv(ctx context.Context) ([]string, error) { childEnv := os.Environ() subscriptionId := a.env.GetSubscriptionId() - for key, value := range a.env.Dotenv() { + for key, value := range environment.FilterLoaderControlKeys(a.env.Dotenv()) { resolved := value if keyvault.IsSecretReference(value) { secret, err := a.keyvaultService.SecretFromKeyVaultReference(ctx, value, subscriptionId) diff --git a/cli/azd/pkg/environment/environment.go b/cli/azd/pkg/environment/environment.go index 520ca93366c..b6531826826 100644 --- a/cli/azd/pkg/environment/environment.go +++ b/cli/azd/pkg/environment/environment.go @@ -307,6 +307,33 @@ func (e *Environment) SetServiceProperty(serviceName string, propertyName string e.DotenvSet(fmt.Sprintf("SERVICE_%s_%s", Key(serviceName), propertyName), value) } +// isLoaderControlKey reports whether key is in the reserved dynamic linker/loader namespace +// (LD_ for Linux ld.so, DYLD_ for macOS dyld), matched case-insensitively. Variables in this +// namespace change how a process resolves and loads shared libraries at launch, so they are not +// carried from an environment's dotenv into the tools azd runs. Names outside the namespace, such +// as LDFLAGS or LDLIBS (no trailing underscore), are not matched. +func isLoaderControlKey(key string) bool { + upper := strings.ToUpper(key) + return strings.HasPrefix(upper, "LD_") || strings.HasPrefix(upper, "DYLD_") +} + +// FilterLoaderControlKeys returns a copy of env with dynamic linker/loader control variables (the +// reserved LD_/DYLD_ namespace, see [isLoaderControlKey]) removed. Use it when building the +// environment for a tool subprocess from an azd environment's dotenv values (see +// [Environment.Dotenv]) so those variables are not propagated. [Environment.Environ] applies the +// same filtering when it builds the subprocess environment slice. +func FilterLoaderControlKeys(env map[string]string) map[string]string { + filtered := make(map[string]string, len(env)) + for k, v := range env { + if isLoaderControlKey(k) { + continue + } + filtered[k] = v + } + + return filtered +} + // 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. // @@ -317,12 +344,7 @@ func (e *Environment) Environ() []string { defer e.mu.RUnlock() envVars := []string{} for k, v := range e.dotenv { - // Skip dynamic linker/loader control variables (the reserved LD_/DYLD_ namespace): they - // change how a process resolves and loads shared libraries at launch, so they must not be - // carried from the dotenv into tool subprocess environments. Matched case-insensitively; - // LDFLAGS/LDLIBS (no underscore) are intentionally unaffected. - upper := strings.ToUpper(k) - if strings.HasPrefix(upper, "LD_") || strings.HasPrefix(upper, "DYLD_") { + if isLoaderControlKey(k) { continue } 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 f2d7bd7e87c..b5a53c6be22 100644 --- a/cli/azd/pkg/environment/environment_test.go +++ b/cli/azd/pkg/environment/environment_test.go @@ -391,6 +391,32 @@ func TestEnviron_ExcludesLoaderControlKeys(t *testing.T) { require.Contains(t, environ, "LDLIBS=-lfoo") } +// TestFilterLoaderControlKeys verifies the shared helper used by the subprocess-env paths that +// build their environment from Dotenv() (e.g. azd exec, kubectl, container app bicep context) +// removes the reserved LD_/DYLD_ namespace while preserving everything else. +func TestFilterLoaderControlKeys(t *testing.T) { + filtered := FilterLoaderControlKeys(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", + }, filtered) +} + // --- Test 10: Dotenv special characters round-trip --- func TestDotenvSpecialCharacters_RoundTrip(t *testing.T) { diff --git a/cli/azd/pkg/project/service_target_aks.go b/cli/azd/pkg/project/service_target_aks.go index bd85ec71a1c..6618fc9016c 100644 --- a/cli/azd/pkg/project/service_target_aks.go +++ b/cli/azd/pkg/project/service_target_aks.go @@ -252,7 +252,7 @@ func (t *aksTarget) Deploy( artifacts := ArtifactCollection{} // Sync environment - t.kubectl.SetEnv(t.env.Dotenv()) + t.kubectl.SetEnv(environment.FilterLoaderControlKeys(t.env.Dotenv())) // Deploy k8s resources in the following order: // 1. Helm @@ -917,7 +917,7 @@ func (t *aksTarget) setK8sContext( serviceConfig *ServiceConfig, eventName ext.Event, ) error { - t.kubectl.SetEnv(t.env.Dotenv()) + t.kubectl.SetEnv(environment.FilterLoaderControlKeys(t.env.Dotenv())) hasCustomKubeConfig := false // If a KUBECONFIG env var is set, use it. diff --git a/cli/azd/pkg/project/service_target_dotnet_containerapp.go b/cli/azd/pkg/project/service_target_dotnet_containerapp.go index 205d6616f5e..a9f0b25e9ff 100644 --- a/cli/azd/pkg/project/service_target_dotnet_containerapp.go +++ b/cli/azd/pkg/project/service_target_dotnet_containerapp.go @@ -329,7 +329,7 @@ func (at *dotnetContainerAppTarget) Deploy( } } // Add the environment variables from the azd environment - maps.Copy(env, at.env.Dotenv()) + maps.Copy(env, environment.FilterLoaderControlKeys(at.env.Dotenv())) builder := strings.Builder{} err = tmpl.Execute(&builder, struct { From 7c04cceb952599f6062a6bb27fc08501f841d803 Mon Sep 17 00:00:00 2001 From: hemarina Date: Thu, 2 Jul 2026 16:44:41 -0700 Subject: [PATCH 5/6] add cspell --- cli/azd/.vscode/cspell.yaml | 3 +++ 1 file changed, 3 insertions(+) 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: From 1951dc03fb5d94216e93959b60cac81e977e9d53 Mon Sep 17 00:00:00 2001 From: hemarina Date: Thu, 2 Jul 2026 18:02:11 -0700 Subject: [PATCH 6/6] apply on Dotenv so that extensions will also get filtered dotenv --- cli/azd/cmd/exec.go | 2 +- cli/azd/pkg/environment/environment.go | 62 +++++++------------ cli/azd/pkg/environment/environment_test.go | 13 ++-- cli/azd/pkg/project/service_target_aks.go | 4 +- .../service_target_dotnet_containerapp.go | 2 +- 5 files changed, 34 insertions(+), 49 deletions(-) diff --git a/cli/azd/cmd/exec.go b/cli/azd/cmd/exec.go index 2645f5cec5c..8918319fd88 100644 --- a/cli/azd/cmd/exec.go +++ b/cli/azd/cmd/exec.go @@ -118,7 +118,7 @@ func newExecAction( func (a *execAction) buildChildEnv(ctx context.Context) ([]string, error) { childEnv := os.Environ() subscriptionId := a.env.GetSubscriptionId() - for key, value := range environment.FilterLoaderControlKeys(a.env.Dotenv()) { + for key, value := range a.env.Dotenv() { resolved := value if keyvault.IsSecretReference(value) { secret, err := a.keyvaultService.SecretFromKeyVaultReference(ctx, value, subscriptionId) diff --git a/cli/azd/pkg/environment/environment.go b/cli/azd/pkg/environment/environment.go index b6531826826..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,46 +322,15 @@ func (e *Environment) SetServiceProperty(serviceName string, propertyName string e.DotenvSet(fmt.Sprintf("SERVICE_%s_%s", Key(serviceName), propertyName), value) } -// isLoaderControlKey reports whether key is in the reserved dynamic linker/loader namespace -// (LD_ for Linux ld.so, DYLD_ for macOS dyld), matched case-insensitively. Variables in this -// namespace change how a process resolves and loads shared libraries at launch, so they are not -// carried from an environment's dotenv into the tools azd runs. Names outside the namespace, such -// as LDFLAGS or LDLIBS (no trailing underscore), are not matched. -func isLoaderControlKey(key string) bool { - upper := strings.ToUpper(key) - return strings.HasPrefix(upper, "LD_") || strings.HasPrefix(upper, "DYLD_") -} - -// FilterLoaderControlKeys returns a copy of env with dynamic linker/loader control variables (the -// reserved LD_/DYLD_ namespace, see [isLoaderControlKey]) removed. Use it when building the -// environment for a tool subprocess from an azd environment's dotenv values (see -// [Environment.Dotenv]) so those variables are not propagated. [Environment.Environ] applies the -// same filtering when it builds the subprocess environment slice. -func FilterLoaderControlKeys(env map[string]string) map[string]string { - filtered := make(map[string]string, len(env)) - for k, v := range env { - if isLoaderControlKey(k) { - continue - } - filtered[k] = v - } - - return filtered -} - -// 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. // -// Dynamic linker/loader control variables (the reserved LD_/DYLD_ namespace) are omitted so they -// are not carried from the dotenv into the environment of tools azd runs. +// 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 { - if isLoaderControlKey(k) { - continue - } + 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 b5a53c6be22..19ebf335492 100644 --- a/cli/azd/pkg/environment/environment_test.go +++ b/cli/azd/pkg/environment/environment_test.go @@ -391,11 +391,12 @@ func TestEnviron_ExcludesLoaderControlKeys(t *testing.T) { require.Contains(t, environ, "LDLIBS=-lfoo") } -// TestFilterLoaderControlKeys verifies the shared helper used by the subprocess-env paths that -// build their environment from Dotenv() (e.g. azd exec, kubectl, container app bicep context) -// removes the reserved LD_/DYLD_ namespace while preserving everything else. -func TestFilterLoaderControlKeys(t *testing.T) { - filtered := FilterLoaderControlKeys(map[string]string{ +// 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", @@ -414,7 +415,7 @@ func TestFilterLoaderControlKeys(t *testing.T) { "SAFE_VALUE": "keep-me", "LDFLAGS": "-L/opt/lib", "LDLIBS": "-lfoo", - }, filtered) + }, env.Dotenv()) } // --- Test 10: Dotenv special characters round-trip --- diff --git a/cli/azd/pkg/project/service_target_aks.go b/cli/azd/pkg/project/service_target_aks.go index 6618fc9016c..bd85ec71a1c 100644 --- a/cli/azd/pkg/project/service_target_aks.go +++ b/cli/azd/pkg/project/service_target_aks.go @@ -252,7 +252,7 @@ func (t *aksTarget) Deploy( artifacts := ArtifactCollection{} // Sync environment - t.kubectl.SetEnv(environment.FilterLoaderControlKeys(t.env.Dotenv())) + t.kubectl.SetEnv(t.env.Dotenv()) // Deploy k8s resources in the following order: // 1. Helm @@ -917,7 +917,7 @@ func (t *aksTarget) setK8sContext( serviceConfig *ServiceConfig, eventName ext.Event, ) error { - t.kubectl.SetEnv(environment.FilterLoaderControlKeys(t.env.Dotenv())) + t.kubectl.SetEnv(t.env.Dotenv()) hasCustomKubeConfig := false // If a KUBECONFIG env var is set, use it. diff --git a/cli/azd/pkg/project/service_target_dotnet_containerapp.go b/cli/azd/pkg/project/service_target_dotnet_containerapp.go index a9f0b25e9ff..205d6616f5e 100644 --- a/cli/azd/pkg/project/service_target_dotnet_containerapp.go +++ b/cli/azd/pkg/project/service_target_dotnet_containerapp.go @@ -329,7 +329,7 @@ func (at *dotnetContainerAppTarget) Deploy( } } // Add the environment variables from the azd environment - maps.Copy(env, environment.FilterLoaderControlKeys(at.env.Dotenv())) + maps.Copy(env, at.env.Dotenv()) builder := strings.Builder{} err = tmpl.Execute(&builder, struct {