Repository navigation
Provider plugin processes keep running after Terraform panics (os.Exit(11) in PanicHandler skips plugin.CleanupClients) #39399
Description
Terraform Version
Terraform v1.16.6
on linux_amd64
+ provider registry.terraform.io/hashicorp/random v3.9.1Also reproduced with v1.15.8, v1.15.9, and a build of main at 9ec49e0 (v1.18.0-dev).
Terraform Configuration Files
main.tf:
terraform {
required_providers {
random = {
source = "hashicorp/random"
}
}
}
resource "random_password" "this" {
count = 1
length = 16
}tests/main.tftest.hcl:
mock_provider "random" {}
run "assertion_fails" {
command = apply
assert {
condition = length(random_password.this) == 0
error_message = "expected no password"
}
}The failing assertion triggers the panic tracked in #39365. That panic is used here only because it is a deterministic way to make Terraform crash while a provider plugin is running.
Debug Output
A full TF_LOG=trace log is available on request. Panic excerpt:
panic: unexpected error marshalling value: value has marks, so it cannot be serialized as JSON
goroutine 86 [running]:
runtime/debug.Stack()
runtime/debug/stack.go:26 +0x5e
github.com/hashicorp/terraform/internal/logging.PanicHandler()
github.com/hashicorp/terraform/internal/logging/panic.go:84 +0x15c
...
github.com/hashicorp/terraform/internal/command/views/json.NewDiagnostic({0x4ab02a0, 0x35a38c928120}, 0x35a38ca54c30)
github.com/hashicorp/terraform/internal/command/views/json/diagnostic.go:278 +0x18de
...
github.com/hashicorp/terraform/internal/moduletest/graph.(*NodeTestRun).Execute(0x35a38c4c2cf0, 0x35a38bfc1260)
github.com/hashicorp/terraform/internal/moduletest/graph/node_test_run.go:147 +0x4b8
github.com/hashicorp/terraform/internal/moduletest/graph.Walk.func1({0x4445fa0, 0x35a38c4c2cf0})
github.com/hashicorp/terraform/internal/moduletest/graph/test_graph_builder.go:130 +0x257
In the trace log, the random plugin is started (provider: plugin started: ... pid=...) and never logs plugin process exited.
Expected Behavior
When Terraform crashes, it still terminates the provider plugin processes it started, as it does on a normal exit. No terraform-provider-* process outlives the terraform process.
Actual Behavior
terraform test exits with status 11 after the crash banner. The terraform-provider-random process it started keeps running. It is reparented to the init or subreaper process and is still running two minutes later:
$ terraform test; echo "exit=$?"
...
exit=11
$ ps -o pid,ppid,stat,etime,rss,comm -p 3250962 # 10 s after exit
PID PPID STAT ELAPSED RSS COMMAND
3250962 594 Sl 00:10 21504 terraform-provi
$ ps -o pid,ppid,stat,etime,rss,comm -p 3250962 # 120 s after exit
PID PPID STAT ELAPSED RSS COMMAND
3250962 594 Sl 02:00 10496 terraform-provi
PID 594 is the session's subreaper. The process has to be killed by hand. With a large provider such as hashicorp/aws, each orphan holds roughly 375-380 MB RSS. Repeated crashes in CI or in scripted test loops accumulate these processes.
Steps to Reproduce
- Save the two files above.
terraform initterraform test; echo "exit=$?"printsexit=11after the crash banner.pgrep -a terraform-providerlists therandomplugin that is still running.
No credentials are needed.
Additional Context
The plugin is orphaned only when a provider instance is still alive at the moment of the panic. In terraform test, that is the case when the test file declares a provider or mock_provider block:
| Test file declares | Panic | Orphaned plugins |
|---|---|---|
| no provider block (per-run provider) | yes | 0 |
provider "random" {} |
yes | 1 |
mock_provider "random" {} |
yes | 1 |
TestProvidersTransformer (internal/moduletest/graph/transform_providers.go) starts file-level providers while it builds the graph. They are closed only by NodeProviderClose, after all runs. Per-run providers are closed before the run's result is rendered, so they are already gone when the panic happens.
The orphaning itself is not specific to terraform test:
- Worker goroutines defer
logging.PanicHandler(). That function ends withos.Exit(11)(internal/logging/panic.go:89). os.Exitskips deferred calls, includingdefer plugin.CleanupClients()inrealMain(main.go:248).- Managed plugins (
internal/command/meta_providers.go:467) are therefore never killed.
The same defer logging.PanicHandler() pattern guards goroutines in internal/terraform/graph.go, internal/terraform/context.go, internal/backend/local/backend_plan.go, backend_apply.go and backend_refresh.go. A panic in any of them would orphan running plugins. Only the terraform test path is demonstrated here.
A local experiment that calls plugin.CleanupClients() in PanicHandler immediately before os.Exit(11) removed the orphans in every case tried. Those cases were the configuration above, the same with provider "random" {}, plan mode, and a module using hashicorp/aws under mock_provider. The exit status stayed 11 and the crash report was unchanged. Whether that is the right place for the cleanup is open for discussion.
References
- terraform test panics ("value has marks") when a failed assertion references a whole object that has deprecated attributes #39365: the
terraform testpanic used as the trigger here. - terraform test crashes when an assertion fails on a deprecation-marked value #39239: the same panic. It notes that the crash also skips the test's apply cleanup.
Generative AI / LLM assisted development?
An LLM coding agent helped narrow down the reproduction and read the source. Every result above was run and checked on the listed versions.
Activity
manenim commentedon Oct 16, 2026
Hi, I’d like to work on this if it’s still available.
I’d start with a regression test that checks for provider processes left running after a panic. Then I’d work on the cleanup path so those processes are terminated before Terraform exits. I’d keep the existing crash report and exit status unchanged.
Please assign this to me if it’s available.
andrewesweet commentedon Oct 17, 2026
@manenim, in case it helps your work on this, some notes from reproducing it locally. Happy for you to take it forward; nothing here is meant to compete with your PR.
The orphan comes from
os.Exit(11)inlogging.PanicHandler, which skips the deferredplugin.CleanupClients()inrealMain. Callingplugin.CleanupClients()inPanicHandlerimmediately beforeos.Exit(11)was enough in my testing: it is a no-op when no managed clients exist, and the crash output and exit status 11 are unchanged. With the reproduction from this issue, three runs each: unpatched left one orphanedterraform-provider-randomprocess per run; patched left none, still exiting 11.On the regression test, the tricky part I hit is that there is no existing deterministic way to make the CLI panic after a provider starts other than the #39365 crash itself, which the fix for that issue (#39406) removes. The options I could see were (a) an e2e test built on the #39365 trigger, which stops panicking once that fix lands; (b) adding a deprecated attribute to a test provider's schema so the trigger survives in test form; (c) a test-only panic hook, which has no precedent here; or (d) no automated test, with before/after evidence. The maintainers may have a preference.
Disclosure: this investigation was done with an AI coding agent and reviewed by me.
manenim commentedon Oct 17, 2026
Thanks @andrewesweet, this is helpful. I appreciate you sharing the reproduction results and the testing options.
I’d like to check whether Terraform’s existing test helpers can support a process-level regression test. It would start a managed test provider, trigger the panic handler, and verify that the provider exits. That would keep the test independent of the crash being fixed in #39406.
Could a maintainer confirm whether this is a suitable direction before I start?