Repository: hashicorp/terraform (Go)

Issue:
Terraform CRASH when upgrading to 1.8 (invalid memory address or nil pointer dereference)
### Terraform Version

```shell
Terraform v1.8.0
on darwin_amd64
+ provider registry.terraform.io/hashicorp/aws v5.39.0
+ provider registry.terraform.io/hashicorp/external v2.3.3
```


### Terraform Configuration Files

```terraform
terraform {
  backend "s3" {
    bucket         = "<redacted>"
    key            = "<redacted>"
    region         = "eu-west-1"
    encrypt        = "true"
    dynamodb_table = "<redacted>"
    profile        = "<redacted>"
  }

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    external = {
      source  = "hashicorp/external"
      version = "~> 2.1"
    }
  }

  required_version = "1.8.0"
}
```


### Debug Output

```
2024-04-15T11:33:09.451+0100 [TRACE] vertex "root": visit complete
2024-04-15T11:33:09.451+0100 [INFO]  backend/local: plan calling Plan
2024-04-15T11:33:09.452+0100 [DEBUG] Building and walking plan graph for NormalMode

!!!!!!!!!!!!!!!!!!!!!!!!!!! TERRAFORM CRASH !!!!!!!!!!!!!!!!!!!!!!!!!!!!

Terraform crashed! This is always indicative of a bug within Terraform.
Please report the crash with Terraform[1] so that we can fix this.

When reporting bugs, please include your terraform version, the stack trace
shown below, and any additional information which may help replicate the issue.

[1]: https://github.com/hashicorp/terraform/issues

!!!!!!!!!!!!!!!!!!!!!!!!!!! TERRAFORM CRASH !!!!!!!!!!!!!!!!!!!!!!!!!!!!

panic: runtime error: invalid memory address or nil pointer dereference
goroutine 1473 [running]:
runtime/debug.Stack()
	/Users/runner/hostedtoolcache/go/1.22.1/x64/src/runtime/debug/stack.go:24 +0x5e
github.com/hashicorp/terraform/internal/logging.PanicHandler()
	/Users/runner/work/terraform/terraform/internal/logging/panic.go:84 +0x18b
panic({0x77accc0?, 0x96f4700?})
	/Users/runner/hostedtoolcache/go/1.22.1/x64/src/runtime/panic.go:770 +0x132
github.com/hashicorp/terraform/internal/refactoring.findMoveStatements(0xc000f36540, {0x0?, 0xc00008f700?, 0x10?})
	/Users/runner/work/terraform/terraform/internal/refactoring/move_statement.go:73 +0x387
github.com/hashicorp/terraform/internal/refactoring.findMoveStatements(0xc000ab5500, {0x0?, 0xc000d98460?, 0xc000a38000?})
	/Users/runner/work/terraform/terraform/internal/refactoring/move_statement.go:96 +0x5f7
github.com/hashicorp/terraform/internal/refactoring.FindMoveStatements(...)
	/Users/runner/work/terraform/terraform/internal/refactoring/move_statement.go:45
github.com/hashicorp/terraform/internal/terraform.(*Context).prePlanFindAndApplyMoves(0xc001047e80, 0xc000ab5500, 0xc0039669c0)
	/Users/runner/work/terraform/terraform/internal/terraform/context_plan.go:508 +0x3f
github.com/hashicorp/terraform/internal/terraform.(*Context).planWalk(0xc001047e80, 0xc000ab5500, 0xc000b3c7b0, 0xc00111e1b0)
	/Users/runner/work/terraform/terraform/internal/terraform/context_plan.go:632 +0x11b
github.com/hashicorp/terraform/internal/terraform.(*Context).plan(0x0?, 0x0?, 0x0?, 0xc00008fe10?)
	/Users/runner/work/terraform/terraform/internal/terraform/context_plan.go:372 +0x1c
github.com/hashicorp/terraform/internal/terraform.(*Context).PlanAndEval(0xc001047e80, 0xc000ab5500, 0xc000b3c7b0, 0xc00111e1b0)
	/Users/runner/work/terraform/terraform/internal/terraform/context_plan.go:247 +0x837
github.com/hashicorp/terraform/internal/terraform.(*Context).Plan(...)
	/Users/runner/work/terraform/terraform/internal/terraform/context_plan.go:140
github.com/hashicorp/terraform/internal/backend/local.(*Local).opPlan.func2()
	/Users/runner/work/terraform/terraform/internal/backend/local/backend_plan.go:108 +0xe9
created by github.com/hashicorp/terraform/internal/backend/local.(*Local).opPlan in goroutine 66
	/Users/runner/work/terraform/terraform/internal/backend/local/backend_plan.go:104 +0x7b7
```

### Expected Behavior

A plan should have been generated without error

### Actual Behavior

Terraform crashed

### Steps to Reproduce

1. Change `required_version` from `1.7.4` to `1.8.0`
2. Run `terraform init`
3. Run `terraform plan`

### Additional Context

_No response_

### References

- #34992 


Discussion:
I believe I have resolved the issue. As suggested in #34992 I think there was a previous bug that would mask this error.

For anyone seeing a similar error, here is a simplified outline of our configuration & the fix:

```
.
├── dev
│   ├── config.tf
│   └──main.tf
└── modules
    └── deployment
        └── wafv2.tf
```

`main.tf` contains
```
module "deployment" {
  source = "../modules/deployment"
  ...
}
```

Inside `module/deployment/wafv2.tf` we moved a resource using the following syntax
```
moved {
  from = module.deployment.aws_wafv2_ip_set.original
  to   = module.deployment.aws_wafv2_ip_set.new
}
```

The issue is that we shouldn't have specified `module.deployment` from _inside_ the module. The fixed code now reads
```
moved {
  from = aws_wafv2_ip_set.original
  to   = aws_wafv2_ip_set.new
}
```
Thanks @AndyDean96, I'll just reopen this as the root cause for this actually different from the alternate bug you linked. We have a fix for this one in progress which I've now linked.

As you spotted Terraform couldn't resolve the module you were targeting as the addresses were slightly wrong. Terraform shouldn't have crashed in this situation however, which is what we'll fix in the linked PR.

Thanks again!
I'm going to lock this issue because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.



Patch:
diff --git a/internal/refactoring/move_statement.go b/internal/refactoring/move_statement.go
index 113c8ae1dc50..eec12bffc457 100644
--- a/internal/refactoring/move_statement.go
+++ b/internal/refactoring/move_statement.go
@@ -64,27 +64,29 @@ func findMoveStatements(cfg *configs.Config, into []MoveStatement) []MoveStateme
 
 		// We have the statement, let's see if we should attach a provider to
 		// it.
-
 		if toResource, ok := mc.To.ConfigMoveable(addrs.RootModule).(addrs.ConfigResource); ok {
 			// Only attach providers if we are moving resources, and we attach
 			// the to resource provider from the config. We can retrieve the
 			// from resource provider from the state later.
+			modCfg := cfg.Descendent(toResource.Module)
+			// It's possible that multiple refactorings have left a moved block
+			// that points to a module which no longer exists. This may also be
+			// a mistake, but the user will see the unexpected deletion in the
+			// plan if it is.
+			if modCfg != nil {
+				resourceConfig := modCfg.Module.ResourceByAddr(toResource.Resource)
+				if resourceConfig != nil {
+					// Check the target resource config actually exists before we
+					// try and extract the provider from them.
+
+					stmt.Provider = &addrs.AbsProviderConfig{
+						Module:   modAddr,
+						Provider: resourceConfig.Provider,
+					}
 
-			resourceConfig := cfg.Descendent(toResource.Module).Module.ResourceByAddr(toResource.Resource)
-			if resourceConfig != nil {
-
-				// Check the target resource config actually exists before we
-				// try and extract the provider from them. If the resource
-				// doesn't exist in config, then we'll get a validation error
-				// later on anyway.
-
-				stmt.Provider = &addrs.AbsProviderConfig{
-					Module:   modAddr,
-					Provider: resourceConfig.Provider,
-				}
-
-				if resourceConfig.ProviderConfigRef != nil {
-					stmt.Provider.Alias = resourceConfig.ProviderConfigRef.Alias
+					if resourceConfig.ProviderConfigRef != nil {
+						stmt.Provider.Alias = resourceConfig.ProviderConfigRef.Alias
+					}
 				}
 			}
 		}


Test patch:
diff --git a/internal/refactoring/testdata/move-statement-implied/move-statement-implied.tf b/internal/refactoring/testdata/move-statement-implied/move-statement-implied.tf
index 4ea628ea65f4..b8ea759a224f 100644
--- a/internal/refactoring/testdata/move-statement-implied/move-statement-implied.tf
+++ b/internal/refactoring/testdata/move-statement-implied/move-statement-implied.tf
@@ -52,3 +52,8 @@ resource "foo" "ambiguous" {
 module "child" {
   source = "./child"
 }
+
+moved {
+  from = foo.already_moved
+  to = module.no_longer_exists.foo.already_moved
+}
diff --git a/internal/terraform/context_plan2_test.go b/internal/terraform/context_plan2_test.go
index 2433b36a3ea3..bdc061e73171 100644
--- a/internal/terraform/context_plan2_test.go
+++ b/internal/terraform/context_plan2_test.go
@@ -1308,6 +1308,71 @@ func TestContext2Plan_movedResourceBasic(t *testing.T) {
 	})
 }
 
+func TestContext2Plan_movedResourceMissingModule(t *testing.T) {
+	addrA := mustResourceInstanceAddr("test_object.a")
+	addrB := mustResourceInstanceAddr("module.gone.test_object.b")
+	m := testModuleInline(t, map[string]string{
+		"main.tf": `
+			moved {
+				from = test_object.a
+				to   = module.gone.test_object.b
+			}
+		`,
+	})
+
+	state := states.BuildState(func(s *states.SyncState) {
+		// The prior state tracks test_object.a, which we should treat as
+		// test_object.b because of the "moved" block in the config.
+		s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{
+			AttrsJSON: []byte(`{}`),
+			Status:    states.ObjectReady,
+		}, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`))
+	})
+
+	p := simpleMockProvider()
+	ctx := testContext2(t, &ContextOpts{
+		Providers: map[addrs.Provider]providers.Factory{
+			addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
+		},
+	})
+
+	plan, diags := ctx.Plan(m, state, &PlanOpts{
+		Mode: plans.NormalMode,
+		ForceReplace: []addrs.AbsResourceInstance{
+			addrA,
+		},
+	})
+	if diags.HasErrors() {
+		t.Fatalf("unexpected errors\n%s", diags.Err().Error())
+	}
+
+	t.Run(addrA.String(), func(t *testing.T) {
+		instPlan := plan.Changes.ResourceInstance(addrA)
+		if instPlan != nil {
+			t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB)
+		}
+	})
+	t.Run(addrB.String(), func(t *testing.T) {
+		instPlan := plan.Changes.ResourceInstance(addrB)
+		if instPlan == nil {
+			t.Fatalf("no plan for %s at all", addrB)
+		}
+
+		if got, want := instPlan.Addr, addrB; !got.Equal(want) {
+			t.Errorf("wrong current address\ngot:  %s\nwant: %s", got, want)
+		}
+		if got, want := instPlan.PrevRunAddr, addrA; !got.Equal(want) {
+			t.Errorf("wrong previous run address\ngot:  %s\nwant: %s", got, want)
+		}
+		if got, want := instPlan.Action, plans.Delete; got != want {
+			t.Errorf("wrong planned action\ngot:  %s\nwant: %s", got, want)
+		}
+		if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseNoMoveTarget; got != want {
+			t.Errorf("wrong action reason\ngot:  %s\nwant: %s", got, want)
+		}
+	})
+}
+
 func TestContext2Plan_movedResourceCollision(t *testing.T) {
 	addrNoKey := mustResourceInstanceAddr("test_object.a")
 	addrZeroKey := mustResourceInstanceAddr("test_object.a[0]")