Skip to content

sfs: provider panics with a nil pointer dereference when the API returns an undecodable response #1743

Description

@devpie

Description

The SFS resources and data sources crash the provider process with a nil pointer dereference when an API call
returns a response that the SDK decodes to nil. Terraform reports it as The plugin encountered an error, and failed to respond to the plugin6.(*GRPCProvider).ReadResource call, so the operator gets a plugin crash instead of
a diagnostic.

Two routes lead to a nil response, neither of which requires anything unusual from the SFS API:

1. A 2xx answer with an empty body. The generated SDK decoder returns early for an empty body
(v1api/client.go):

func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) {
	if len(b) == 0 {
		return nil
	}

localVarReturnValue therefore stays nil and Execute() returns (nil, nil).

2. A single retryable gateway error during a wait. WaiterHelper.Wait returns true for waitFinished on any
fetch error, handleError swallows a 502 or 504 and returns a nil error, and the loop then returns the nil response
because it is already done — the retry never happens. Reported separately as
stackitcloud/stackit-sdk-go#11084 with a runnable reproduction.

The call sites check only the returned error. Several of the checks that are meant to validate a response are
themselves the dereference — they test x.Field == nil without first testing x == nil:

// stackit/internal/services/sfs/resourcepool/resource.go, v0.113.0
if response.ResourcePool == nil || response.ResourcePool.Id == nil {   // panics when response is nil

and in both create paths the wait handler result is read by tflog.SetField one line before its guard.

Steps to reproduce

A plain Terraform configuration cannot trigger this deterministically — it needs the API to answer a GET with an
empty body or a single 502, which is not something a config can provoke. The configuration involved is unremarkable:

resource "stackit_sfs_resource_pool" "this" {
  project_id        = var.project_id
  name              = "sfs-pool"
  availability_zone = "eu01-m"
  performance_class = "Standard"
  size_gigabytes    = 512
  ip_acl            = ["192.168.2.0/24"]
}

The crash reproduces deterministically as a unit test against a mock server. Add this to
stackit/internal/services/sfs/sfs_test.go and run
go test ./stackit/internal/services/sfs/ -run TestSfsResourcePoolReadHandlesEmptyResponse:

func TestSfsResourcePoolReadHandlesEmptyResponse(t *testing.T) {
	projectId := uuid.NewString()
	resourcePoolId := uuid.NewString()
	const region = "eu01"

	s := testutil.NewMockServer(t)
	defer s.Server.Close()
	tfConfig := fmt.Sprintf(`
provider "stackit" {
	default_region = "%s"
	sfs_custom_endpoint = "%s"
	service_account_token = "mock-server-needs-no-auth"
	enable_beta_resources = true
}
resource "stackit_sfs_resource_pool" "resourcepool" {
  project_id        = "%s"
  name              = "sfs-instance"
  availability_zone = "eu01-m"
  performance_class = "Standard"
  size_gigabytes    = 512
  ip_acl            = ["192.168.2.0/24"]
}
`, region, s.Server.URL, projectId)

	resource.UnitTest(t, resource.TestCase{
		ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
		Steps: []resource.TestStep{
			{
				// Fail the create wait so the IDs are in state and the next step can refresh them.
				PreConfig: func() {
					s.Reset(
						testutil.MockResponse{
							Description: "create resource pool",
							ToJsonBody: sfs.CreateResourcePoolResponse{
								ResourcePool: &sfs.ResourcePool{Id: new(resourcePoolId)},
							},
						},
						testutil.MockResponse{Description: "failing waiter", StatusCode: http.StatusInternalServerError},
					)
				},
				Config:      tfConfig,
				ExpectError: regexp.MustCompile("Error creating resource pool"),
			},
			{
				PreConfig: func() {
					s.Reset(
						testutil.MockResponse{Description: "refresh with an empty body", StatusCode: http.StatusOK},
						testutil.MockResponse{Description: "delete", StatusCode: http.StatusAccepted},
						testutil.MockResponse{Description: "delete waiter", StatusCode: http.StatusNotFound},
					)
				},
				RefreshState: true,
				ExpectError:  regexp.MustCompile("Error reading resource pool"),
			},
		},
	})
}

Actual behavior

The provider process panics:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x105620e48]

github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sfs/resourcepool.(*resourcePoolResource).Read(...)
	stackit/internal/services/sfs/resourcepool/resource.go:375
github.com/hashicorp/terraform-plugin-framework/internal/fwserver.(*Server).ReadResource(...)
	terraform-plugin-framework@v1.19.0/internal/fwserver/server_readresource.go:156

Line 375 is err = mapFields(ctx, region, response.ResourcePool, &model); on the released v0.113.0 it is line 372.

Affected sites, all of them reachable by one of the two routes above:

file method
sfs/resourcepool/resource.go Create (wait result, post-create GET), Read, Update (PATCH response, wait result)
sfs/resourcepool/datasource.go Read
sfs/share/resource.go Create (POST response, wait result, post-create GET), Read, Update (PATCH response, wait result)
sfs/share/datasource.go Read
sfs/snapshots/datasource.go Read

export-policy and snapshot-policy already guard their responses, project-lock uses nil-receiver-safe getters,
and every SFS Delete discards the response, so the delete waiter's legitimate nil return on a 404 is harmless.

Expected behavior

A response the SDK could not decode should produce a Terraform diagnostic, not a plugin crash. The package already
has the right shape for this in resourcepool/resource.go — one combined condition covering the response and the
fields actually used:

if resourcePool == nil || resourcePool.ResourcePool == nil || resourcePool.ResourcePool.Id == nil {
	core.LogAndAddError(ctx, &resp.Diagnostics, "error creating resource pool", "Calling API: Incomplete response (id missing)")
	return
}

Environment

  • OS: macOS 15 (darwin/arm64)
  • Terraform version (see terraform --version): reproduced through terraform-plugin-testing v1.16.0; not version-specific
  • Version of the STACKIT Terraform provider: v0.113.0, and main at a7d114b

Additional information

A fix is open as #1741. It guards the call sites so the provider reports a diagnostic; the underlying
(nil, nil) return from the wait handler is stackitcloud/stackit-sdk-go#11084.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions