Skip to content

Commit cc7ea5a

Browse files
committed
fix: don't leak the wrapper's own internal state into the child process
appmap-python is a plain Python script, so it gets self-instrumented at interpreter startup via appmap.pth (AppMap instruments any process by default), before runner.py's own code has computed the environment the target command should actually run with. That incidental self-init writes internal, process-scoped state under _APPMAP*-prefixed env var names using whatever it inherited — notably _APPMAP_MESSAGES_SHOWN, the once-per-process guard around the startup config-dump log lines. Since os.execvpe inherits the current environment, that stale marker carried into the exec'd child, so anyone using the wrapper (e.g. `appmap-python --enable-log flask run`) never saw the config-dump log lines, even with everything else configured correctly: the wrapper's own throwaway initialization had already tripped the "already shown" guard before the child process's real initialization ran. Strip all _APPMAP*-prefixed env vars before exec'ing the child and let the already-computed envvars re-set whatever it actually needs, so the child always starts from a clean slate regardless of what the wrapper's incidental self-init happened to do. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Nw1eEFHvABs5hdjMej5bf
1 parent 1264fd4 commit cc7ea5a

2 files changed

Lines changed: 28 additions & 1 deletion

File tree

_appmap/test/test_runner.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import re
23

34
import pytest
@@ -64,3 +65,19 @@ def test_recording_type_present(self, script_runner):
6465
)
6566
assert result.returncode == 0
6667
assert re.match(r"true", result.stdout) is not None
68+
69+
def test_internal_state_not_leaked_to_child(self, script_runner):
70+
# appmap-python is itself instrumented at interpreter startup (via
71+
# appmap.pth), which can set internal, process-scoped _APPMAP*
72+
# markers using whatever it inherited, before this script has
73+
# computed the environment the child command should actually run
74+
# with. Simulate that by pre-setting one such marker (the
75+
# once-per-process "startup messages already shown" guard) and
76+
# confirm it doesn't leak into the child's environment, which would
77+
# otherwise silently suppress the child's own startup logging.
78+
env = {**os.environ, "_APPMAP_MESSAGES_SHOWN": "true"}
79+
result = script_runner.run(
80+
["appmap-python", "printenv", "_APPMAP_MESSAGES_SHOWN"], env=env
81+
)
82+
assert result.returncode != 0
83+
assert result.stdout == ""

appmap/command/runner.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,17 @@ def run():
130130
print(f"{k}={v}")
131131
sys.exit(0)
132132

133-
os.execvpe(cmd[0], cmd, {**os.environ, **envvars})
133+
# appmap-python is itself instrumented on interpreter startup (via
134+
# appmap.pth), before this point, using whatever environment it inherited
135+
# rather than the envvars computed above. That incidental self-init can
136+
# set internal, process-scoped state under _APPMAP*-prefixed names (e.g.
137+
# the once-per-process "startup messages already shown" guard); left in
138+
# place, it would carry over into the child's environment and suppress
139+
# or corrupt the child's own startup behavior. Drop all of it and let
140+
# envvars below re-set whatever the child actually needs.
141+
child_env = {k: v for k, v in os.environ.items() if not k.startswith("_APPMAP")}
142+
child_env.update(envvars)
143+
os.execvpe(cmd[0], cmd, child_env)
134144

135145

136146
if __name__ == "__main__":

0 commit comments

Comments
 (0)