| service | lambda | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| sdk_module | aws-sdk-go-v2/service/lambda@v1.101.2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| last_audit_commit | a007ec3e | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| last_audit_date | 2026-07-25 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| overall | A | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| protocol | REST-JSON | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| families |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gaps | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deferred | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| leaks |
|
- InvocationType is a type alias (type InvocationType = string) so lambda backend satisfies sns.LambdaInvoker directly.
- ARN-parsing anti-pattern "take last colon segment" recurs — watch for it elsewhere.
- Trap: RemovePermission wire = DELETE /2015-03-31/functions/{name}/policy/{StatementId} (path, not query).
- ce30166a (Parity sweep 3, unrelated commit that swept in a large dependency+datalayer PR) converted most lambda backend maps to pkgs/store Table/Index. eventInvokeConfigs, versions, layers, versionCounters, functionConcurrencies, layerVersionCounters, layerPolicies, activeConcurrencies, fnCodeSigningConfigs, fisFaults, runtimeManagementConfigs, functionRecursionConfigs, functionScalingConfigs, versionIndex, esmByFunctionARN, runtimes, functionURLServers were deliberately left as plain maps (documented per-field in store_setup.go's package doc) — each has a concrete reason (no pure identity in the value, one-to-many shape, or live non-serializable state). Read that doc comment before "fixing" any of them into a Table.
- pkgs/store.Table/Index perform NO internal locking (by design — see pkgs/store package doc); every lambda call site still takes b.mu itself. Index.Get() returns a slice OWNED BY THE INDEX — never return it directly from a public method without copying first (ListAliases/GetPolicy both copy correctly; verified).
- Policy RevisionId (function-policy and layer-version-policy) is deliberately a pure content-hash of the sorted StatementId set (policyRevisionID in permissions.go, layerPolicyRevisionID in layers.go), NOT a stored uuid.New()-per-mutation field like Function/Version/Alias RevisionID. This works because statement content is immutable once added (no UpdatePermission op exists — a StatementId can only be added once, then removed), so the ID set alone detects every real mutation, and it stays correct across Snapshot/Restore without adding new persisted state.
- writeError's return value is NOT a reliable "did this write an error response" signal — c.JSON (which it wraps) returns nil on any successful write, including a written error, so
if xErr := h.writeError(...); xErr != nilcan never trigger. Handler helpers that write an error and need the caller to stop must return bool (true=continue), matching validateMemoryAndTimeout/checkRevisionID/applyFunctionCodeUpdate. A stale!= nilcheck on such a helper is a latent double-write bug (found + fixed in applyFunctionCodeUpdate this sweep) — grep for this pattern before trusting any "returns error, checked with != nil" helper that calls writeError internally. - Durable-execution family spans THREE independent path prefixes, not one — do not assume everything nests under
/2025-12-01/durable-executions/{DurableExecutionArn}/...: GetDurableExecution/History/State + CheckpointDurableExecution + StopDurableExecution do; ListDurableExecutionsByFunction is/2025-12-01/functions/{FunctionName}/durable-executions(a/functionspath, verified against api_op_ListDurableExecutionsByFunction.go); SendDurableExecutionCallback{Success,Failure,Heartbeat} is/2025-12-01/durable-execution-callbacks/{CallbackId}/{succeed|fail|heartbeat}keyed by CallbackId, not DurableExecutionArn (note succeed/fail, not success/failure — trap for anyone guessing the suffix). See handler_paths.go's prefix constants and handler_durable_execution.go'sisDurableExecPath/dispatchDurableExecRoutes. - Lambda's REST API is spread across a dozen+ date-versioned path prefixes (2015-03-31, 2017-03-31, 2017-10-31, 2018-10-31, 2019-09-25, 2019-09-30, 2020-04-22, 2020-06-30, 2021-07-20, 2021-10-31, 2021-11-15, 2024-08-31, 2025-11-30, 2025-12-01 all appear). gopherstack-l5ir found 4 of these constants carrying a wrong date (tags: 2015-03-31 vs real 2017-03-31; recursion-config: 2024-08-28 vs real 2024-08-31; scaling-config: 2023-10-26 vs real 2025-11-30) that made every op under that prefix unreachable. When adding or auditing any lambda op, verify its date prefix against
httpbinding.SplitURI(...)in serializers.go directly -- do not assume a "close enough" date is correct, and do not trust an existing constant's date without checking it against the SDK source at least once. - durable_execution is intentionally NOT wired into Snapshot/Restore (durableExecutionStore isn't touched by persistence.go) — this predates the wire-shape rewrite and is unrelated to it; durable executions were never persisted, only cleared on Reset (lifecycle.go's
b.durableExecs.reset()). Not flagged as a bug: no entry point exists to repopulate FunctionArn/DurableConfig/InputPayload after a restore anyway (see durable_execution family note above), so persisting the store today would only round-trip empty shells. ListLayersandListLayerVersionssummary narrowing:LayerVersion.Contentwas previously populated onListLayersandListLayerVersionsresponses. Inaws-sdk-go-v2/service/lambda@v1.101.2,types.LayerVersionsListItemdoes not containContent(onlyGetLayerVersion/PublishLayerVersionreturnsContent). Fixed:ListLayersandListLayerVersionsomitContent.
2026-08-23: pagination bug sweep (ListLayerVersions, ListProvisionedConcurrencyConfigs, ListCodeSigningConfigs, ListFunctionsByCodeSigningConfig)
Discovered while auditing the pagination bug class found in medialive.
handleListLayerVersions, handleListProvisionedConcurrencyConfigs,
handleListCodeSigningConfigs, and handleListFunctionsByCodeSigningConfig
all ignored the real Marker/MaxItems request members (lambda@v1.101.2:
ListLayerVersionsInput, ListProvisionedConcurrencyConfigsInput,
ListCodeSigningConfigsInput, ListFunctionsByCodeSigningConfigInput) and
always returned every item in one unbounded page with no NextMarker,
despite NextMarker already existing (unused) on all four output structs.
Fixed using the existing parsePaginationParams + pkgs/page.New +
lambdaDefaultMaxItems pattern already used by ListFunctions/ListLayers
in this package. ListLayerVersions, ListProvisionedConcurrencyConfigs,
and ListFunctionsByCodeSigningConfig are unexported *InMemoryBackend
methods (not part of a public interface) but changed return type from a
bare slice to page.Page[T]; go build ./... confirmed clean, and two
pre-existing test call sites (persistence_test.go, layers_test.go) updated
for the new ListLayerVersions signature. Proven with four
Test*_SDKRoundTrip_Pagination tests (list_pagination_ignored_test.go),
each driving the real SDK client across two 10-item pages of 25 seeded
items and asserting the pages are disjoint; all four fail against the
unfixed handlers (should have 10 item(s), but has 25), hand-reverted
and confirmed.
Audited but NOT fixed: handleListFunctionURLConfigs also ignores
Marker/MaxItems, but the route is always called with a non-empty
{name} path segment, and the per-function code path
(GetFunctionURLConfig(name)) can only ever return 0 or 1 items — this
service's data model has no per-qualifier function URL configs, so the
unbounded branch is dead code with zero real blast radius. Not fixed.
Read serializeOpHttpBindings<Op>Input directly for DeleteFunctionInput
(lambda@v1.101.2 serializers.go:1690,
awsRestjson1_serializeOpHttpBindingsDeleteFunctionInput): FunctionName
is URI-bound, Qualifier is query-bound
(encoder.SetQuery("Qualifier")). handleDeleteFunction
(handler_functions.go) never read the query string at all — it called
h.Backend.DeleteFunction(name) unconditionally, so a client asking to
delete one published version (DeleteFunctionInput{FunctionName, Qualifier: "2"}) instead had the entire function deleted: every version,
every alias, every event source mapping. api_op_DeleteFunction.go's doc
comment is explicit: "To delete a specific function version, use the
Qualifier parameter. Otherwise, all versions and aliases are deleted", and
"You can't delete a version that an alias references." The backend already
tracked exactly the state this needed (b.versionIndex/b.versions for
published versions, b.aliasesByFunction for the alias-reference check) —
only DeleteFunction's dispatch ignored the qualifier.
Fixed via the existing QualifierInvoker/QualifierResolver
optional-extension pattern (store.go) rather than changing
StorageBackend.DeleteFunction's existing signature (would have required
touching services/cloudformation/resources.go:2150, the one out-of-package
caller, and running make build-check): added QualifierDeleter with
DeleteFunctionVersion(name, qualifier string) error, implemented on
InMemoryBackend (functions.go). handleDeleteFunction now reads
Qualifier off the query string; when present it type-asserts
QualifierDeleter and calls DeleteFunctionVersion, which deletes only the
targeted b.versionIndex[name][qualifier] entry (and its b.versions[name]
slice element) after checking b.aliasesByFunction for a referencing alias
(ErrVersionReferencedByAlias, new sentinel → 409 ResourceConflictException)
and rejecting Qualifier=$LATEST (ErrInvalidParameterValue → 400 — $LATEST
has no separate version resource; omit Qualifier to delete the whole
function). An empty Qualifier still calls the original unqualified
DeleteFunction path unchanged. Function tags are only released when the
whole function is deleted (qualifier == "").
TestDeleteFunction_Qualifier (delete_function_version_test.go) drives
the real aws-sdk-go-v2 lambda client, table-driven across three cases:
qualified delete removes only the targeted version ($LATEST and the other
version survive, GetFunctionConfiguration(Qualifier: v1) now 404s);
qualified delete is rejected with ResourceConflictException when an alias
still references that version (and the version survives the rejected
delete); unqualified delete still removes the whole function. Hand-reverted
handleDeleteFunction back to its pre-fix unconditional
h.Backend.DeleteFunction(name) call: both the "removes only that version"
and "blocked by alias reference" subtests failed exactly as predicted (the
whole function vanished instead of just the targeted version, so
GetFunctionConfiguration against the survivor 404'd and the
expected-error assertion against the alias-referenced delete saw no error
at all); restored and confirmed byte-identical via md5sum.
Modelling gaps found in the same header sweep, not implemented:
InvokeInput's TenantId (lambda@v1.101.2 serializers.go:3859,
awsRestjson1_serializeOpHttpBindingsInvokeInput) is a real
X-Amz-Tenant-Id header for Lambda's multi-tenant-function feature —
gopherstack has no tenant concept anywhere in this service, so this is a
genuine unmodeled feature, not a discarded-but-tracked field; reported, not
attempted. InvokeInput.DurableExecutionName (request header) and
InvokeOutput.DurableExecutionArn (response header, deserializers.go:8744,
awsRestjson1_deserializeOpHttpBindingsInvokeOutput) are likewise never
wired on the Invoke path — consistent with, not a new instance of, the
already-documented durable_execution family gap above ("gopherstack has no
StartDurableExecution entry point... this emulator's Invoke path does not
model durable-execution semantics").
Gates: go build ./..., go vet ./services/lambda/..., go test -race -count=1 ./services/lambda/..., go fix -diff ./services/lambda/... (no
diff), gofmt -l services/lambda/ (no output), golangci-lint run ./services/lambda/... (1 finding — godot on the new
DeleteFunctionVersion doc comment's closing quoted sentence, fixed by
rewording so the comment's last line ends outside the quote; 0 issues after,
no //nolint added), go test ./pkgs/persistence/... (no persisted struct
changed) all clean. No exported method signature was changed —
StorageBackend.DeleteFunction is untouched — so make build-check was not
required; go build ./... (whole repo) confirmed clean regardless.