Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 17 additions & 19 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,23 @@ jobs:
- name: gcc-7
- name: gcc-8
- name: gcc-11
steps:
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ env.CRT_CI_ROLE }}
aws-region: ${{ env.AWS_DEFAULT_REGION }}
- name: Build ${{ env.PACKAGE_NAME }}
run: |
aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh
./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --compiler=${{ matrix.compiler.name }} --cmake-extra=-DASSERT_LOCK_HELD=ON --cmake-extra=-DAWS_ENABLE_S3_ENDPOINT_RESOLVER=ON

linux-newer-compiler-compat:
runs-on: ubuntu-24.04 # latest
strategy:
fail-fast: false
matrix:
compiler:
- name: gcc-13
# See Issue: https://github.com/llvm/llvm-project/issues/59007. Although this issue
# has been fixed in LLVM, the fix will probably not propagate to older versions of Ubuntu and GCC 13.1.
#
# Starting with GLIBC version 2.34, the `dn_expand` function, previously found in `libresolv.so`, was moved to `libc.so`. This
# function is used internally by the `getaddrinfo()` system call.
#
# In our setup (As of December 2024), we are using an Ubuntu 18 Docker image on a newer Ubuntu host.
# However, due to compatibility issues between newer libasan.so in GCC 13.1
# and the older Ubuntu image, the linker does not link with `libresolv.so`.
# This results in crashes in `getaddrinfo()` since Ubuntu-18 GLIBC is 2.31.
#
# This problem does not occur on Ubuntu 22 and newer because GLIBC versions 2.34
# and above include `dn_expand` in `libc.so`, eliminating the dependency on
# `libresolv.so`.
#
# We can bypass this problem by linking with "resolv" manually until we bump
# our base Linux image to Ubuntu 22.
extra-build-flag: --cmake-extra=-DCMAKE_EXE_LINKER_FLAGS="-lresolv"
steps:
- uses: aws-actions/configure-aws-credentials@v6
with:
Expand All @@ -96,7 +94,7 @@ jobs:
- name: Build ${{ env.PACKAGE_NAME }}
run: |
aws s3 cp s3://aws-crt-test-stuff/ci/${{ env.BUILDER_VERSION }}/linux-container-ci.sh ./linux-container-ci.sh && chmod a+x ./linux-container-ci.sh
./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-${{ env.LINUX_BASE_IMAGE }} build -p ${{ env.PACKAGE_NAME }} --compiler=${{ matrix.compiler.name }} ${{ matrix.compiler.extra-build-flag }} --cmake-extra=-DASSERT_LOCK_HELD=ON --cmake-extra=-DAWS_ENABLE_S3_ENDPOINT_RESOLVER=ON
./linux-container-ci.sh ${{ env.BUILDER_VERSION }} aws-crt-ubuntu-22-x64 build -p ${{ env.PACKAGE_NAME }} --compiler=${{ matrix.compiler.name }} --cmake-extra=-DASSERT_LOCK_HELD=ON --cmake-extra=-DAWS_ENABLE_S3_ENDPOINT_RESOLVER=ON

clang-sanitizers:
runs-on: ubuntu-24.04 # latest
Expand Down
34 changes: 34 additions & 0 deletions include/aws/s3/private/s3_checksums.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ struct aws_checksum_vtable {
void (*destroy)(struct aws_s3_checksum *checksum);
int (*update)(struct aws_s3_checksum *checksum, const struct aws_byte_cursor *buf);
int (*finalize)(struct aws_s3_checksum *checksum, struct aws_byte_buf *out);
/* Optional. NULL for algorithms that cannot be combined. See aws_checksum_combine_digest. */
int (*combine)(struct aws_s3_checksum *head, uint64_t tail_value, uint64_t tail_length);
};

struct aws_s3_checksum {
Expand Down Expand Up @@ -222,6 +224,38 @@ int aws_checksum_update(struct aws_s3_checksum *checksum, const struct aws_byte_
AWS_S3_API
int aws_checksum_finalize(struct aws_s3_checksum *checksum, struct aws_byte_buf *output);

/**
* True if checksums of the algorithm can be combined via aws_checksum_combine_digest.
* Only the CRC algorithms (CRC32, CRC32C, CRC64NVME) can be.
*/
AWS_S3_API
bool aws_checksum_algorithm_is_combinable(enum aws_s3_checksum_algorithm algorithm);

/* Largest digest produced by an algorithm that satisfies aws_checksum_algorithm_is_combinable.
* The combinable algorithms are all CRCs, so sizeof(uint64_t) covers CRC64NVME and CRC32s. */
#define AWS_S3_COMBINABLE_DIGEST_MAX_LEN sizeof(uint64_t)

/**
* Folds the digest of one data block into `head`, so that `head` becomes the checksum of its own data
* followed by that block, without re-scanning either:
*
* head = checksum(block_head)
* tail_digest = checksum_finalize(block_tail)
* aws_checksum_combine_digest(head, tail_digest, block_tail_length)
* -> head == checksum(block_head || block_tail)
*
* Taking a digest rather than a live checksum lets the caller fold in a block long after the
* checksum that produced it is gone. `tail_length` is the length in bytes of the data that produced
* `tail_digest`, not the digest size.
*
* `head`'s algorithm must satisfy aws_checksum_algorithm_is_combinable.
* AWS_ERROR_UNSUPPORTED_OPERATION for a non-combinable algorithm,
* AWS_ERROR_INVALID_STATE if `head` is already finalized,
* AWS_ERROR_INVALID_ARGUMENT if `tail_digest` is not exactly the algorithm's digest size.
*/
AWS_S3_API
int aws_checksum_combine_digest(struct aws_s3_checksum *head, struct aws_byte_cursor tail_digest, uint64_t tail_length);

AWS_S3_API
int aws_s3_meta_request_checksum_config_storage_init(
struct aws_allocator *allocator,
Expand Down
53 changes: 53 additions & 0 deletions include/aws/s3/private/s3_meta_request_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ struct aws_s3_prepare_request_payload {
void *user_data;
};

/* One part's contribution to the meta request's whole-object checksum. Lives in
* aws_s3_meta_request.combine_slots at index (part_number - 1).
*
* Deliberately plain data: it owns nothing and points at nothing, so its lifetime is the meta
* request's and is independent of the request. That is what lets a request tear down its own running
* checksums at stream completion while its contribution to the whole-object checksum outlives it. */
struct aws_s3_combine_slot {
/* Length in bytes of the part body the digest covers. */
uint64_t length;
/* Raw (not base64) digest of the part body, `digest_len` bytes. */
uint8_t digest[AWS_S3_COMBINABLE_DIGEST_MAX_LEN];
/* Zero until this part records its digest. A slot still zero when the meta request finishes means
* the part never completed, so the whole-object checksum cannot be assembled. */
size_t digest_len;
};

/* An event to be delivered on the meta-request's io_event_loop thread. */
struct aws_s3_meta_request_event {
enum aws_s3_meta_request_event_type {
Expand Down Expand Up @@ -352,6 +368,23 @@ struct aws_s3_meta_request {
/* running checksum of all the parts of a default get, or ranged get meta request*/
struct aws_s3_checksum *meta_request_level_running_response_sum;

/* True when meta_request_level_running_response_sum uses an algorithm that aws_checksum_combine supports
* (the CRCs). In that case each part computes a digest of its own body on its connection's thread and
* records it in combine_slots, and the whole-object sum is assembled from those digests with an O(1)
* combine per part when the meta request finishes, so no thread re-reads the body. Otherwise the delivery
* thread feeds bytes into the running sum directly, which requires delivery to be in object order. */
bool meta_request_level_checksum_combinable;

/* Per-part digests will be folded into meta_request_level_running_response_sum, indexed by
* (part_number - 1). NULL unless meta_request_level_checksum_combinable is true.
*
* Not protected by the synced data lock, and does not need to be: the array is allocated once, before any
* part is dispatched, and never resized, so slot addresses are stable; each part writes only its own slot
* and touches no shared bookkeeping. */
struct aws_s3_combine_slot *combine_slots;
/* Number of entries in combine_slots. Zero when combine_slots is NULL. */
uint32_t combine_slot_count;

/* The receiving file handler */
FILE *recv_file;
struct aws_string *recv_filepath;
Expand Down Expand Up @@ -482,6 +515,26 @@ void aws_s3_meta_request_stream_response_body_synced(
struct aws_s3_meta_request *meta_request,
struct aws_s3_request *request);

/* Decides how the whole-object response checksum will be built, once the discovery response has told us which
* algorithm the object uses and how many parts it has. Call after aws_s3_check_headers_for_checksum() has run
* at the meta request level, before any part is dispatched, with the meta request's synced data lock HELD.
*
* For the CRCs, each part checksums its own body on its own connection's thread and records the digest in
* meta_request->combine_slots, which this function allocates. The digests are folded together with an O(1)
* combine per part when the meta request finishes, so no thread ever re-reads the object. Everything else
* falls back to feeding the running sum from the delivery thread.
*
* `discovery_request` is the request whose response headers were just inspected. If it carried body bytes of
* its own (a partNumber=1 GET rather than a HEAD), its digest is computed here, since its body may arrive before
* the algorithm was known.
*
* Only worth calling for multipart downloads; with a single request there are no parts to combine. */
AWS_S3_API
int aws_s3_meta_request_setup_checksum_combine_synced(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

do we know the timings on this? if needed we can probably speed it up further

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[checksum-combine] 9363 combines in 3.293 ms file(s) remaining
[checksum-combine] 65536 combines in 21.310 msile(s) remaining 

~350ns per combine operations. So, for the case of small part size on large object, it took 20ms, but not i'd say to be a concern here.

struct aws_s3_meta_request *meta_request,
struct aws_s3_request *discovery_request,
uint32_t total_num_parts);

/* Add an event for delivery on the meta-request's io_event_loop thread.
* These events usually correspond to callbacks that must fire sequentially and non-overlapping,
* such as delivery of a part's response body. */
Expand Down
14 changes: 13 additions & 1 deletion include/aws/s3/private/s3_request.h
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,20 @@ struct aws_s3_request {
/* checksum found in the header of an individual get part http request */
struct aws_byte_buf request_level_response_header_checksum;

/* running checksum of the response to an individual get part http request */
/* Running checksum used to validate this part against its own checksum header, which
* request_level_response_header_checksum holds the expected value for. NULL when the response carried no
* checksum header of its own. Its algorithm is whichever one that header named. */
struct aws_s3_checksum *request_level_running_response_sum;

/* Running checksum used only to produce this part's digest for the meta request's whole-object combine.
* NULL when this part does not contribute to a combine.
*
* Kept separate from request_level_running_response_sum because the two answer to different algorithms:
* this one always uses the whole-object algorithm from the discovery response, while the validation sum
* uses whatever algorithm this part's own checksum header named. Nothing requires those to agree — an
* object can carry a whole-object CRC64NVME while its parts carry per-part CRC32 — and folding a digest
* of the wrong algorithm would silently corrupt the whole-object sum. */
struct aws_s3_checksum *request_level_combine_sum;
/* The algorithm used to validate the checksum */
enum aws_s3_checksum_algorithm validation_algorithm;

Expand Down
9 changes: 9 additions & 0 deletions source/s3_auto_ranged_get.c
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,15 @@ static void s_s3_auto_ranged_get_request_finished(
object_range_start,
object_range_end);
}

/* Only now is the part count known, which is what sizes the per-part checksum slots. Deciding
* here also means every part dispatched afterwards sees the decision already made. */
if (meta_request->checksum_config.validate_response_checksum && error_code == AWS_ERROR_SUCCESS) {
if (aws_s3_meta_request_setup_checksum_combine_synced(
meta_request, request, auto_ranged_get->synced_data.total_num_parts) != AWS_OP_SUCCESS) {
error_code = aws_last_error_or_unknown();
}
}
}

switch (request->request_tag) {
Expand Down
62 changes: 62 additions & 0 deletions source/s3_checksums.c
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,22 @@ static void s_crc_destroy(struct aws_s3_checksum *checksum) {
aws_mem_release(checksum->allocator, checksum);
}

static int s_crc32_combine(struct aws_s3_checksum *head, uint64_t tail_value, uint64_t tail_length) {
head->impl.crc_val_32bit = aws_checksums_crc32_combine(head->impl.crc_val_32bit, (uint32_t)tail_value, tail_length);
return AWS_OP_SUCCESS;
}

static int s_crc32c_combine(struct aws_s3_checksum *head, uint64_t tail_value, uint64_t tail_length) {
head->impl.crc_val_32bit =
aws_checksums_crc32c_combine(head->impl.crc_val_32bit, (uint32_t)tail_value, tail_length);
return AWS_OP_SUCCESS;
}

static int s_crc64nvme_combine(struct aws_s3_checksum *head, uint64_t tail_value, uint64_t tail_length) {
head->impl.crc_val_64bit = aws_checksums_crc64nvme_combine(head->impl.crc_val_64bit, tail_value, tail_length);
return AWS_OP_SUCCESS;
}

static struct aws_checksum_vtable hash_vtable = {
.update = s_hash_update,
.finalize = s_hash_finalize,
Expand All @@ -269,16 +285,19 @@ static struct aws_checksum_vtable crc32_vtable = {
.update = s_crc32_checksum_update,
.finalize = s_crc32_finalize,
.destroy = s_crc_destroy,
.combine = s_crc32_combine,
};
static struct aws_checksum_vtable crc32c_vtable = {
.update = s_crc32c_checksum_update,
.finalize = s_crc32_finalize,
.destroy = s_crc_destroy,
.combine = s_crc32c_combine,
};
static struct aws_checksum_vtable crc64nvme_vtable = {
.update = s_crc64nvme_checksum_update,
.finalize = s_crc64_finalize,
.destroy = s_crc_destroy,
.combine = s_crc64nvme_combine,
};

struct aws_s3_checksum *aws_hash_new(struct aws_allocator *allocator, aws_hash_new_fn hash_fn) {
Expand Down Expand Up @@ -405,6 +424,49 @@ int aws_checksum_finalize(struct aws_s3_checksum *checksum, struct aws_byte_buf
return checksum->vtable->finalize(checksum, output);
}

bool aws_checksum_algorithm_is_combinable(enum aws_s3_checksum_algorithm algorithm) {
switch (algorithm) {
case AWS_SCA_CRC32:
case AWS_SCA_CRC32C:
case AWS_SCA_CRC64NVME:
return true;
default:
return false;
}
}

int aws_checksum_combine_digest(
struct aws_s3_checksum *head,
struct aws_byte_cursor tail_digest,
uint64_t tail_length) {

if (head == NULL || head->vtable == NULL || head->vtable->combine == NULL) {
return aws_raise_error(AWS_ERROR_UNSUPPORTED_OPERATION);
}
if (!head->good) {
return aws_raise_error(AWS_ERROR_INVALID_STATE);
}
if (tail_digest.len != head->digest_size) {
return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
}

/* The CRC finalizers write digests big-endian, so read them back the same way. */
uint64_t tail_value = 0;
if (head->digest_size == AWS_CRC32_LEN) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

potential optimization idea for future: but for gets we never need to finalize to be and then back. thats needed purely for puts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah, mostly because our current aws_s3_checksum interface is a pointer tied with the request itself, so I want to keep the parts level checksum independent from the request without affecting the lifetime of the request itself to keep it simple.

And the interface only has the checksum_finalize that provides the encoded checksum. and I keep a list of the finalized one with the meta request.

But, the extra encode/decode should be trivial, we can optimize it if we need to.

uint32_t value_32 = 0;
if (!aws_byte_cursor_read_be32(&tail_digest, &value_32)) {
return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
}
tail_value = value_32;
} else {
if (!aws_byte_cursor_read_be64(&tail_digest, &tail_value)) {
return aws_raise_error(AWS_ERROR_INVALID_ARGUMENT);
}
}

return head->vtable->combine(head, tail_value, tail_length);
}

static int s_checksum_compute_fn(
struct aws_allocator *allocator,
const struct aws_byte_cursor *input,
Expand Down
Loading