From 8010552d4361b7cb393d74a0c8f2046349ff444d Mon Sep 17 00:00:00 2001 From: Aditya Jain Date: Fri, 14 Aug 2026 17:32:59 -0700 Subject: [PATCH] fix: SamConfig.get_all() mutates shared document, leaking one command's params into another's global_params obtained in get_all() was a live reference into self.document rather than a copy. Calling .update() on it permanently merged the current command's section-specific values into the shared global section, and since self.document is cached across calls (_read() only re-reads when empty), a subsequent get_all() for a different command would silently inherit the previous command's parameter values instead of the true global default. Fixes #9181 --- samcli/lib/config/samconfig.py | 6 ++-- tests/unit/lib/samconfig/test_samconfig.py | 35 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/samcli/lib/config/samconfig.py b/samcli/lib/config/samconfig.py index 2cd0d41199a..5d2467cf3c8 100644 --- a/samcli/lib/config/samconfig.py +++ b/samcli/lib/config/samconfig.py @@ -89,9 +89,9 @@ def get_all(self, cmd_names, section, env=DEFAULT_ENV): config_content = self.document.get(env, {}) params = config_content.get(self.to_key(cmd_names), {}).get(section, {}) if DEFAULT_GLOBAL_CMDNAME in config_content: - global_params = config_content.get(DEFAULT_GLOBAL_CMDNAME, {}).get(section, {}) - global_params.update(params.copy()) - params = global_params.copy() + global_params = dict(config_content.get(DEFAULT_GLOBAL_CMDNAME, {}).get(section, {})) + global_params.update(params) + params = global_params return params def put(self, cmd_names, section, key, value, env=DEFAULT_ENV): diff --git a/tests/unit/lib/samconfig/test_samconfig.py b/tests/unit/lib/samconfig/test_samconfig.py index c58f0709a61..71a94c5fbc3 100644 --- a/tests/unit/lib/samconfig/test_samconfig.py +++ b/tests/unit/lib/samconfig/test_samconfig.py @@ -178,6 +178,41 @@ def test_dedup_global_param(self): self.samconfig.document["myEnv"][DEFAULT_GLOBAL_CMDNAME]["mySection"], {"testKey": "ValueFromGlobal"} ) + def test_get_all_does_not_leak_command_params_into_global_across_calls(self): + """Regression test: a prior get_all() call for one command must not pollute the + global section that a later get_all() call for a different command inherits from. + """ + self._update_samconfig( + cmd_names=[DEFAULT_GLOBAL_CMDNAME], + section="parameters", + key="stack_name", + value="global-stack", + env="myEnv", + ) + self._update_samconfig( + cmd_names=["deploy"], section="parameters", key="stack_name", value="deploy-only-stack", env="myEnv" + ) + self._update_samconfig(cmd_names=["deploy"], section="parameters", key="region", value="us-east-1", env="myEnv") + + self.assertEqual( + {"stack_name": "deploy-only-stack", "region": "us-east-1"}, + self.samconfig.get_all(cmd_names=["deploy"], section="parameters", env="myEnv"), + ) + + # "build" has no command-specific parameters, so it must resolve to only the + # true global default. It must not inherit "deploy"'s command-specific values. + self.assertEqual( + {"stack_name": "global-stack"}, + self.samconfig.get_all(cmd_names=["build"], section="parameters", env="myEnv"), + ) + + # Calling get_all() for "deploy" again must still return deploy's own values, + # not a further-corrupted mix. + self.assertEqual( + {"stack_name": "deploy-only-stack", "region": "us-east-1"}, + self.samconfig.get_all(cmd_names=["deploy"], section="parameters", env="myEnv"), + ) + def test_check_config_get(self): self._setup_config() self.assertEqual(