Skip to content

fix(lambda): support S3 code packages for CreateFunction and UpdateFunctionCode (#34) - #51

Open
tyrchen wants to merge 2 commits into
masterfrom
feat/lambda-s3-code-packages
Open

fix(lambda): support S3 code packages for CreateFunction and UpdateFunctionCode (#34)#51
tyrchen wants to merge 2 commits into
masterfrom
feat/lambda-s3-code-packages

Conversation

@tyrchen

@tyrchen tyrchen commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #34CreateFunction / UpdateFunctionCode accepted Code.S3Bucket/S3Key but never downloaded the object, so the first Invoke failed with InvalidCode("missing code root") and GetFunction reported a fake S3 code_location. Standard AWS SDK clients (e.g. AWS SDK for Java v2) that deploy from S3 were silently broken.

This PR makes S3 code packages fully functional: the object is downloaded at create/update time and stored through the exact same path as inline ZipFile (store_zip_code), so checksum, size, extraction, and executor behavior are identical. Invalid S3 references now fail fast at creation time with AWS-compatible InvalidParameterValueException messages instead of failing later at invoke.

Design

Follows the repo's established cross-service bridge pattern (SNS→SQS SqsPublisher, EventBridge→SQS TargetDelivery): a trait in the consuming core crate, an implementation in the server binary, no core→core dependency.

  • rustack-lambda-core::code — new S3CodeFetcher trait (async-trait, object-safe) + S3CodeFetchError (thiserror) + UnavailableS3CodeFetcher default that produces a clear "S3 service is not enabled" error when no S3 backend is wired.
  • ProviderRustackLambda gains code_fetcher + with_code_fetcher; process_code takes a CodeSource bundling the mutually-exclusive code inputs. Fail-fast validation: ZipFile/S3Bucket/ImageUri mutual exclusion, S3Key required with S3Bucket, empty/oversize S3 fields rejected (byte caps per AGENTS.md).
  • Bridgeapps/rustack/src/lambda_s3_bridge.rs implements S3CodeFetcher over the in-process RustackS3 provider. Bucket + version resolution mirrors handle_get_object: latest non-delete-marker via get, explicit versions via get_version, delete-marker rejection, "null" unversioned ids, parking_lot guard dropped before any .await.
  • Wiringmain.rs builds the S3 provider before Lambda and selects the bridge when S3 is enabled, else the unavailable fetcher (feature-flag safe).

Spec

specs/ruststack-lambda-s3-code-design.md — design, error model, testing plan, exit criteria. Stale "does not fetch from S3" notes in ruststack-lambda-design.md / ruststack-lambda-executor-design.md updated.

Verification

  • 13 new lambda-core unit tests (mock fetcher: happy path, NoSuchBucket/NoSuchKey/NoSuchVersion mapping, mutual exclusion, empty/oversize fields, unavailable fetcher, update-from-S3)
  • 7 bridge unit tests (round-trip, missing bucket/key/version, delete marker, versioned latest vs pinned, "null" version)
  • 2 SDK integration tests (test_lambda.rs — create with S3 code asserts populated code metadata; missing bucket asserts InvalidParameterValueException)
  • 1 native-executor round-trip test (test_lambda_invoke.rs — real bootstrap zip uploaded to S3, function created from S3 code, invoke → 200 echo)
  • End-to-end (manual): zip → rustack S3 → CreateFunction with S3 code → Invoke{"echo":{"hello":"from-s3"}} HTTP 200; missing bucket → HTTP 400 NoSuchBucket message
  • Quality gates: cargo build / cargo test (workspace) / cargo +nightly fmt --check / cargo clippy -- -D warnings (1.95 pinned + current on changed crates) / RUSTDOCFLAGS="-D warnings" cargo doc — all green.

Notes

  • The pre-existing working-tree deletion of rust-toolchain.toml (not part of this change) was left unstaged.
  • External S3 endpoints (LocalStack/real AWS as the code source) are out of scope; the trait is the seam for a future HTTP-backed fetcher (see spec §8).

…nctionCode (#34)

CreateFunction/UpdateFunctionCode accepted Code.S3Bucket/S3Key but never
downloaded the object, so the first Invoke failed with
InvalidCode("missing code root") and GetFunction reported a fake S3
location. Standard AWS SDK clients (e.g. Java v2) that deploy from S3 were
silently broken.

What landed:
- New S3CodeFetcher trait + S3CodeFetchError in rustack-lambda-core::code
  (async-trait; object-safe dyn dispatch); the default
  UnavailableS3CodeFetcher rejects S3 packages with a clear error when no
  S3 backend is wired.
- RustackLambda gains a code_fetcher field + with_code_fetcher builder;
  process_code takes a CodeSource bundling the mutually-exclusive code
  inputs and stores fetched bytes via the same store_zip_code path as
  inline ZipFile (same checksum, size, extraction, executor behavior).
- Fail-fast validation at create/update time: ZipFile/S3Bucket/ImageUri
  mutual exclusion, S3Key required with S3Bucket, empty and oversize S3
  fields rejected, AWS-compatible NoSuchBucket/NoSuchKey/NoSuchVersion
  error messages (InvalidParameterValueException, HTTP 400).
- apps/rustack::lambda_s3_bridge implements S3CodeFetcher over the
  in-process RustackS3 provider (bucket + version resolution mirroring
  handle_get_object, including delete-marker rejection and "null"
  unversioned ids); main.rs wires it when S3 is enabled, else the
  unavailable fetcher. No core->core dependency.
- Spec: specs/ruststack-lambda-s3-code-design.md (design + testing plan +
  exit criteria); stale "does not fetch from S3" notes updated in
  ruststack-lambda-design.md and ruststack-lambda-executor-design.md.

Verified:
- 13 new lambda-core unit tests, 7 bridge unit tests, 2 SDK integration
  tests (test_lambda.rs), 1 native-executor S3-code round-trip test
  (test_lambda_invoke.rs).
- End-to-end: zip uploaded to rustack S3, function created from S3 code,
  invoked -> 200 with echo payload; missing bucket -> 400
  InvalidParameterValueException.
- Gates: cargo build, cargo test (workspace), cargo +nightly fmt --check,
  cargo clippy -D warnings (1.95 pinned + 1.97 on changed crates),
  RUSTDOCFLAGS="-D warnings" cargo doc.

Note: the pre-existing working-tree deletion of rust-toolchain.toml (not
part of this change) was left unstaged; .DS_Store likewise.

Closes #34

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c90cfdbf9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2635 to +2639
let zip_bytes = self
.code_fetcher
.fetch_code(bucket, key, source.s3_object_version)
.await
.map_err(map_s3_code_fetch_error)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce the package limit before storing S3 bytes

When an S3 object exceeds MAX_ZIP_SIZE or is a compressed archive that expands dramatically, this call materializes the entire object and then passes it to store_zip_code, which writes and extracts it before CreateFunction checks the compressed size; UpdateFunctionCode never performs that check at all. Consequently, an untrusted S3 package can consume unbounded memory/disk, and oversized updates are accepted successfully. Check the fetched length before storage and enforce an extracted-byte limit while unpacking.

AGENTS.md reference: AGENTS.md:L125-L125

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0359966: check_code_size now runs inside process_code for both the inline ZipFile and S3 branches — before any store_zip_code write/extraction — so oversized packages fail fast on both CreateFunction and UpdateFunctionCode (which previously had no size check at all). Additionally, extract_zip now caps extracted bytes at 250 MB (MAX_EXTRACTED_SIZE, mirroring the AWS unzipped limit): each entry's declared size is pre-checked against the remaining budget and actual copied bytes are bounded via Read::take, rejecting zip bombs with InvalidZipFile. New tests: oversize-on-create, oversize-on-update, extraction budget accept/reject.

Comment thread apps/rustack/src/lambda_s3_bridge.rs Outdated
Comment on lines +82 to +83
.read_object(bucket, key, &storage_version_id, None)
.await

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add a timeout around the S3 object read

When an S3-backed package has spilled to disk and that I/O stalls, this unbounded await keeps the CreateFunction or UpdateFunctionCode request open indefinitely because the gateway has no enclosing request timeout. Wrap the read in a configured tokio::time::timeout and map expiry to a service error.

AGENTS.md reference: AGENTS.md:L122-L122

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0359966: the read_object await in LambdaS3CodeFetcher::fetch_code is now wrapped in tokio::time::timeout(DEFAULT_READ_TIMEOUT) (30s const in the bridge), mapping expiry to S3CodeFetchError::Internal with a descriptive message → InvalidParameterValueException at the provider boundary.

Comment on lines +2854 to +2855
if let Some(version) = source.s3_object_version {
validate_s3_field("Code.S3ObjectVersion", version, MAX_S3_VERSION_LEN)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject an object version without an S3 location

When S3ObjectVersion is supplied alongside ZipFile or ImageUri but no S3Bucket/S3Key, validation only checks its length and the request succeeds while silently ignoring the version. Treat the version as part of the S3 source and require both bucket and key, just as the existing checks do for either location field.

AGENTS.md reference: AGENTS.md:L99-L99

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0359966: validate_code_source now rejects S3ObjectVersion unless both S3Bucket and S3Key are present ("Code.S3ObjectVersion requires Code.S3Bucket and Code.S3Key"), instead of silently ignoring the version next to ZipFile/ImageUri. Covered by test_should_reject_s3_version_without_location.

Addresses the Codex review on PR #51:

- Enforce the 50 MB zipped package limit BEFORE any storage or extraction
  (check_code_size in process_code, applied to both inline ZipFile and S3
  code paths, so UpdateFunctionCode is covered too — previously it had no
  size check at all and oversized CreateFunction packages were written and
  extracted before being rejected).
- Cap extracted bytes at 250 MB during zip extraction (store_zip_code /
  extract_zip) to defeat zip-bomb expansion; declared entry size is
  pre-checked against the remaining budget and actual copied bytes are
  bounded.
- Bound the S3 object read with a 30s tokio::time::timeout in the bridge
  (DEFAULT_READ_TIMEOUT) so a stalled disk I/O cannot hold CreateFunction /
  UpdateFunctionCode open indefinitely; expiry maps to a service error.
- Reject S3ObjectVersion without a full S3 location (bucket + key), instead
  of silently ignoring it next to ZipFile/ImageUri.
- New tests: oversize S3 code on create and update, version without
  location, extraction budget accept/reject, delete-marker pinning.
  Spec updated to match (size check, extraction cap, timeout, version rule).

Verified: workspace tests green (135 lambda-core + 51 app), clippy -D
warnings on 1.95 (pinned) and 1.97 for changed crates, RUSTDOCFLAGS="-D
warnings" cargo doc, and the native S3-code round-trip integration test.

Closes #34

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 03599661f0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1053 to 1054
let mut extracted_total: u64 = 0;
for i in 0..archive.len() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cap the number of extracted ZIP entries

A package can remain below both byte limits while containing hundreds of thousands of empty files or directories, and this loop creates every entry without decrementing extracted_total; an S3-backed deployment can therefore exhaust inodes or occupy a blocking worker for a long time despite the new ZIP-bomb guard. Reject archives above a bounded entry count before creating their paths.

AGENTS.md reference: AGENTS.md:L103-L103

Useful? React with 👍 / 👎.

Comment on lines +611 to +612
let (code_sha256, code_size, zip_bytes, code_path, image_uri) =
self.process_code(&name, "$LATEST", code_source).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify the function before materializing S3 code

When UpdateFunctionCode names a nonexistent function, this call downloads and extracts the S3 package before self.store.update discovers that the record is absent. Each distinct nonexistent name therefore leaves an orphaned code directory containing up to 250 MB of extracted data, so repeated requests referencing the same compressed object can exhaust disk; resolve the record before processing code or clean up on the not-found path.

Useful? React with 👍 / 👎.

Comment on lines +991 to +993
let extract_result = tokio::task::spawn_blocking(move || {
extract_zip(&bytes_owned, &extract_to, MAX_EXTRACTED_SIZE)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stage extraction before replacing live code

When an update archive exceeds MAX_EXTRACTED_SIZE, this newly fallible extraction runs only after store_zip_code has overwritten code.zip, removed the existing $LATEST/extracted directory, and begun writing the replacement. Returning the size error leaves the function record pointing at its old deployment while the on-disk code is partial, so the rejected update can break subsequent cold invocations; extract into a temporary directory and atomically replace the live package only after validation succeeds.

Useful? React with 👍 / 👎.

Comment on lines +611 to +612
let (code_sha256, code_size, zip_bytes, code_path, image_uri) =
self.process_code(&name, "$LATEST", code_source).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject S3 code updates for image functions

When the target function was created with PackageType: Image, an S3 update reaches this call and subsequently stores ZIP bytes/path while clearing image_uri, but never changes or validates record.latest.package_type. The resulting record still selects the Image execution path with no image URI, so later invocations fail; require S3/Zip sources for Zip functions and ImageUri for Image functions before processing the update.

AGENTS.md reference: AGENTS.md:L107-L107

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CreateFunction accepts S3Bucket/S3Key code but never downloads it, causing "missing code root" at invoke time

1 participant