Describe the bug
A client configured for Requester Pays (i.e. sending x-amz-request-payer: requester) succeeds on GetObject, PutObject, and HeadObject against a Requester Pays bucket, but its CopyObject meta-request fails with 403 Forbidden — even for a same-bucket copy where the caller holds both s3:GetObject and s3:PutObject.
Root cause
Every CopyObject meta-request first issues a HEAD on the source object to learn its size, before any small-vs-multipart branching (source/s3_copy_object.c, AWS_S3_COPY_OBJECT_REQUEST_TAG_GET_OBJECT_SIZE). That HEAD is built in aws_s3_get_source_object_size_message_new() (source/s3_request_messages.c) from a fresh aws_http_message that inherits none of the original request's headers — so x-amz-request-payer is never set on it. On a Requester Pays bucket the un-headered HEAD is rejected with 403, failing the whole copy before any other sub-request runs.
This is unique to the size-HEAD. The other copy sub-requests forward headers and are unaffected:
- Small-object
BYPASS (CopyObject) uses aws_s3_message_util_copy_http_message_no_body_all_headers → copies all headers.
CreateMultipartUpload / UploadPartCopy / CompleteMultipartUpload / AbortMultipartUpload use exclusion-list copies that do not exclude x-amz-request-payer.
It is also why plain GetObject / PutObject / HeadObject (and the auto-ranged GET/PUT paths) work — they copy all headers from the template message.
Reproduction
- Cross-account: bucket in account A with Requester Pays enabled; caller in account B granted
s3:GetObject + s3:PutObject via bucket policy.
- Configure the client to send
x-amz-request-payer: requester.
GetObject / PutObject / HeadObject → OK. CopyObject (e.g. a same-bucket rename) → 403.
Proposed fix
In aws_s3_get_source_object_size_message_new(), forward the request-scoped headers from the original request onto the source-size HEAD, in both the copy_source_uri branch and the x-amz-copy-source fallback branch:
x-amz-request-payer → forwarded verbatim.
x-amz-source-expected-bucket-owner → mapped to x-amz-expected-bucket-owner on the HEAD. This is the header that asserts the source owner on a CopyObject; the plain x-amz-expected-bucket-owner asserts the destination owner and must not be forwarded onto a read of the source object, or legitimate cross-account copies would newly 403.
Motivating context
Surfaced by awslabs/s3-connector-for-pytorch#435 (adds a requester_pays client option; closes awslabs/s3-connector-for-pytorch#430). With that option enabled, GET/PUT/HEAD work but S3FileSystem.rename (implemented as copy + delete) 403s on Requester Pays buckets. The connector reaches aws-c-s3 through mountpoint-s3 → mountpoint-s3-crt-sys, so the fix belongs here.
Environment
- aws-c-s3:
main (reproduced at commit 448ec5e)
- Also affects the version vendored by
mountpoint-s3-crt-sys 0.13.1.
A fix plus regression test is ready; PR to follow.
Describe the bug
A client configured for Requester Pays (i.e. sending
x-amz-request-payer: requester) succeeds onGetObject,PutObject, andHeadObjectagainst a Requester Pays bucket, but itsCopyObjectmeta-request fails with 403 Forbidden — even for a same-bucket copy where the caller holds boths3:GetObjectands3:PutObject.Root cause
Every
CopyObjectmeta-request first issues aHEADon the source object to learn its size, before any small-vs-multipart branching (source/s3_copy_object.c,AWS_S3_COPY_OBJECT_REQUEST_TAG_GET_OBJECT_SIZE). That HEAD is built inaws_s3_get_source_object_size_message_new()(source/s3_request_messages.c) from a freshaws_http_messagethat inherits none of the original request's headers — sox-amz-request-payeris never set on it. On a Requester Pays bucket the un-headered HEAD is rejected with 403, failing the whole copy before any other sub-request runs.This is unique to the size-HEAD. The other copy sub-requests forward headers and are unaffected:
BYPASS(CopyObject) usesaws_s3_message_util_copy_http_message_no_body_all_headers→ copies all headers.CreateMultipartUpload/UploadPartCopy/CompleteMultipartUpload/AbortMultipartUploaduse exclusion-list copies that do not excludex-amz-request-payer.It is also why plain
GetObject/PutObject/HeadObject(and the auto-ranged GET/PUT paths) work — they copy all headers from the template message.Reproduction
s3:GetObject+s3:PutObjectvia bucket policy.x-amz-request-payer: requester.GetObject/PutObject/HeadObject→ OK.CopyObject(e.g. a same-bucket rename) → 403.Proposed fix
In
aws_s3_get_source_object_size_message_new(), forward the request-scoped headers from the original request onto the source-size HEAD, in both thecopy_source_uribranch and thex-amz-copy-sourcefallback branch:x-amz-request-payer→ forwarded verbatim.x-amz-source-expected-bucket-owner→ mapped tox-amz-expected-bucket-owneron the HEAD. This is the header that asserts the source owner on aCopyObject; the plainx-amz-expected-bucket-ownerasserts the destination owner and must not be forwarded onto a read of the source object, or legitimate cross-account copies would newly 403.Motivating context
Surfaced by awslabs/s3-connector-for-pytorch#435 (adds a
requester_paysclient option; closes awslabs/s3-connector-for-pytorch#430). With that option enabled, GET/PUT/HEAD work butS3FileSystem.rename(implemented as copy + delete) 403s on Requester Pays buckets. The connector reaches aws-c-s3 through mountpoint-s3 → mountpoint-s3-crt-sys, so the fix belongs here.Environment
main(reproduced at commit448ec5e)mountpoint-s3-crt-sys0.13.1.A fix plus regression test is ready; PR to follow.