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.
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 asThe plugin encountered an error, and failed to respond to the plugin6.(*GRPCProvider).ReadResource call, so the operator gets a plugin crash instead ofa 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):localVarReturnValuetherefore stays nil andExecute()returns(nil, nil).2. A single retryable gateway error during a wait.
WaiterHelper.WaitreturnstrueforwaitFinishedon anyfetch error,
handleErrorswallows a 502 or 504 and returns a nil error, and the loop then returns the nil responsebecause it is already
done— the retry never happens. Reported separately asstackitcloud/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 == nilwithout first testingx == nil:and in both create paths the wait handler result is read by
tflog.SetFieldone line before its guard.Steps to reproduce
A plain Terraform configuration cannot trigger this deterministically — it needs the API to answer a
GETwith anempty body or a single 502, which is not something a config can provoke. The configuration involved is unremarkable:
The crash reproduces deterministically as a unit test against a mock server. Add this to
stackit/internal/services/sfs/sfs_test.goand rungo test ./stackit/internal/services/sfs/ -run TestSfsResourcePoolReadHandlesEmptyResponse:Actual behavior
The provider process panics:
Line 375 is
err = mapFields(ctx, region, response.ResourcePool, &model); on the releasedv0.113.0it is line 372.Affected sites, all of them reachable by one of the two routes above:
sfs/resourcepool/resource.gosfs/resourcepool/datasource.gosfs/share/resource.gosfs/share/datasource.gosfs/snapshots/datasource.goexport-policyandsnapshot-policyalready guard their responses,project-lockuses nil-receiver-safe getters,and every SFS
Deletediscards 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 thefields actually used:
Environment
terraform --version): reproduced throughterraform-plugin-testingv1.16.0; not version-specificv0.113.0, andmainat a7d114bAdditional 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.