[
https://issues.apache.org/jira/browse/HDDS-17629?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18135627#comment-18135627
]
Ritesh Shukla commented on HDDS-17629:
--------------------------------------
Updated the description after a second test against AWS S3 on 2026-10-31. S3
never refuses a {{Quiet}} value: {{true}} in any letter case means quiet, and
any other text means not quiet, so the proposed fix now reads {{Quiet}} that
way instead of refusing unusual values. For {{PartNumber}}, S3 reads an empty
element as 0 ({{InvalidArgument}}), accepts Unicode decimal digits, and refuses
{{2147483648}} and a {{Part}} with no {{PartNumber}} with {{MalformedXML}}; the
table and the proposed fix now follow those answers.
----
🤖 Generated with [Claude Code|https://claude.com/claude-code]
> S3G: PartNumber and Quiet in request XML are not read as S3 reads them;
> PartNumber 4294967297 is read as 1
> ----------------------------------------------------------------------------------------------------------
>
> Key: HDDS-17629
> URL: https://issues.apache.org/jira/browse/HDDS-17629
> Project: Apache Ozone
> Issue Type: Bug
> Components: S3
> Reporter: Ritesh Shukla
> Assignee: Ritesh Shukla
> Priority: Minor
>
> h3. Problem
> CompleteMultipartUpload joins uploaded parts into one object. The request
> names each part by {{PartNumber}} and {{ETag}}. S3 reads {{PartNumber}}
> strictly. Tested against AWS S3 on 2026-10-31, it refuses {{1 2}}, {{1+2}}
> and {{4294967297}} with 400 {{MalformedXML}}, and {{-1}} with 400
> {{InvalidArgument}} ("PartNumber must be >= 1"). The S3 Gateway reads the
> same texts as 12, 12, 1 and -1, and acts on those numbers.
> Example. An upload has part 1 holding {{a}} and part 12 holding {{b}}. The
> client sends part 12's ETag with a part number of {{1 2}}:
> {code:xml}
> <CompleteMultipartUpload xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
> <Part><PartNumber>1
> 2</PartNumber><ETag>"92eb5ffee6ae2fec3ad71c777531578f"</ETag></Part>
> </CompleteMultipartUpload>
> {code}
> S3 refuses it with 400 {{MalformedXML}}. The gateway reads part 12
> (measured). The ETag matches part 12, so the upload is completed and the
> object holds {{b}} (from code).
> ||{{PartNumber}} text||AWS S3 (tested 2026-10-31)||Ozone today||
> |{{1}}|200, completed with part 1|same|
> |{{01}}, {{+1}}, {{1}} with spaces around it|200, read as part 1|same|
> |{{1 2}}, with part 12's ETag|400 {{MalformedXML}}|reads 12, completes with
> part 12|
> |{{1+2}}, with part 12's ETag|400 {{MalformedXML}}|reads 12, completes with
> part 12|
> |{{4294967297}}, with part 1's ETag|400 {{MalformedXML}}|reads 1, completes
> with part 1|
> |{{-1}}|400 {{InvalidArgument}}, "PartNumber must be >= 1"|reads -1, OM
> answers {{InvalidPart}}|
> |{{0}}|400 {{InvalidArgument}}, "PartNumber must be >= 1"|reads 0, OM answers
> {{InvalidPart}}|
> |{{1+}}, {{1-}}|400 {{MalformedXML}}|reads 1 (completes) and -1
> ({{InvalidPart}})|
> |{{abc}}, {{1.0}}, a no-break space before {{1}}|400 {{MalformedXML}}|reads
> 0, OM answers {{InvalidPart}}|
> |{{2147483648}}|400 {{MalformedXML}}|reads -2147483648, OM answers
> {{InvalidPart}}|
> |{{10001}}, {{2147483647}}|400 {{InvalidPart}} (no such part)|same|
> |an empty element, {{<PartNumber/>}}|400 {{InvalidArgument}}, "PartNumber
> must be >= 1"|reads 0, OM answers {{InvalidPart}}|
> |no {{PartNumber}} element|400 {{MalformedXML}}|reads 0, OM answers
> {{InvalidPart}}|
> |tab and newline around {{1}}|200, read as part 1|same|
> |Arabic-Indic digit one (U+0661)|200, read as part 1|reads 0, OM answers
> {{InvalidPart}}|
> The Ozone readings are measured with master's request classes and namespace
> filter, Hadoop's secure SAX parser and jaxb-runtime 2.3.9, following the
> steps of {{MessageUnmarshaller.readFrom}}. What Ozone does after the reading
> (completing, or OM's answer) follows from the code and was not run.
> DeleteObjects' {{Quiet}} flag goes through the same conversion, with a worse
> result. S3 never refuses a {{Quiet}} value (tested 2026-10-31): {{true}} in
> any letter case means quiet, and any other text means not quiet, including
> {{1}}, {{true}} with spaces around it, {{False}}, {{yes}} and an empty
> element. The gateway reads {{true}}, {{1}} and {{true}} with spaces as quiet,
> and {{false}}, {{0}} and {{2}} as not quiet. Other text, such as {{True}},
> {{TRUE}}, {{False}}, {{yes}}, an empty element or {{<Quiet/>}}, is read as
> nothing (null), which was measured. The gateway reads {{Quiet}} only after OM
> has deleted the keys. So such a request deletes them and then fails with a
> NullPointerException, where S3 answers 200.
> h3. Impact
> * Part numbers. OM uses a part only if the request's ETag matches that part's
> stored ETag ({{S3MultipartUploadCompleteRequest.java:677-692}}, from code).
> So a misread number can only pick a part whose ETag the request names. The
> result is an object that a valid request with the same ETags could also
> build, and the same permissions are needed. What goes wrong: Ozone completes
> uploads that S3 refuses. {{-1}} gets {{InvalidPart}} instead of S3's
> {{InvalidArgument}}. Text such as {{abc}} is read as part 0, so the request
> fails with {{InvalidPart}} because of a part it never named. Two entries that
> convert to the same number, such as {{1}} and {{4294967297}}, are merged into
> one part before OM sees them (from code). HDDS-17554 tracks literal duplicate
> part numbers.
> * Quiet. The keys are deleted, but the caller gets an error instead of the
> delete result. From code, not run: the error is an HTTP 500 because no
> exception mapper handles it, the S3 Gateway audit log has no entry for the
> request (OM still audits the delete), and a retry fails the same way.
> * Who sends these forms. Not the AWS Java SDKs. Both write part numbers with
> Java's {{toString}} and booleans as lowercase {{true}} or {{false}}, checked
> in the bytecode of SDK v1 1.12.788 and SDK v2 2.55.11. The AWS CLI and boto3
> were not checked. Hand-written XML can: a script that prints a boolean the
> way Python or .NET do writes {{True}}.
> * Not affected: lifecycle. Its {{Days}} and {{DaysAfterInitiation}} are
> already read as text and parsed strictly since HDDS-16393. {{1 2}}, {{1+2}},
> {{7+}} and overflow are refused there (measured). Every other request field
> is text.
> h3. Cause
> Gateway files are in
> {{hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint}};
> {{S3MultipartUploadCompleteRequest.java}} is in
> {{hadoop-ozone/ozone-manager}}. Line numbers are for master at 6b78ebd246.
> * {{CompleteMultipartUploadRequest.java:55-56}} binds {{PartNumber}} to an
> {{int}}. {{MultiDeleteRequest.java:36-37}} binds {{Quiet}} to a {{Boolean}}.
> JAXB fills them with its built-in converters ({{DatatypeConverterImpl}} in
> jaxb-runtime 2.3.9). The integer converter skips spaces and plus signs
> anywhere in the text, takes a minus sign anywhere as the sign, and wraps
> around on overflow. The boolean converter returns null for text it does not
> recognise.
> * {{MessageUnmarshaller.java:76-77}} keeps JAXB's default error handling,
> which ignores conversion errors. Text such as {{abc}} leaves {{PartNumber}}
> at its default, 0.
> * Nothing checks the values afterwards. {{ObjectEndpoint.java:872-874}} puts
> each number into a map keyed by part number and sends the map to OM.
> {{BucketEndpoint.java:414}} unboxes {{Quiet}} after the delete call at line
> 407.
> h3. Proposed fix
> Answer as S3 does. Bind both values as text and parse them strictly before
> anything is acted on. Lifecycle already does this for {{Days}} since
> HDDS-16393 ({{S3LifecycleConfiguration.java:154-157}} and {{243-253}}). The
> public getters and setters keep their types.
> {{PartNumber}}, following S3's answers (tested 2026-10-31):
> * Drop XML whitespace (space, tab, CR, LF) around the number and read the
> rest as a 32-bit decimal integer: one optional sign, then digits. S3 reads
> {{01}}, {{+1}} and the Arabic-Indic digit one as part 1, so leading zeros and
> Unicode decimal digits are accepted, as Java's {{Integer.parseInt}} accepts
> them.
> * Refuse anything else with 400 {{MalformedXML}}: whitespace or a sign inside
> the number, a decimal point, letters, a no-break space, a number too large
> for 32 bits ({{2147483648}}, {{4294967297}}), and a {{Part}} with no
> {{PartNumber}} element.
> * Read an empty {{PartNumber}} element as 0, as S3 does, and refuse any
> number below 1 with 400 {{InvalidArgument}} and S3's message, "PartNumber
> must be >= 1".
> * Numbers from 10,001 to 2147483647 stay as today: OM answers
> {{InvalidPart}}, as S3 does.
> Check every part in {{ObjectEndpoint.completeMultipartUpload}}, next to the
> existing empty-part-list check, before OM is called. S3 checks that the
> upload exists before it reads the parts, so for an unknown upload with a
> malformed part number S3 answers {{NoSuchUpload}} where the gateway will
> answer {{MalformedXML}}. That order difference is left as is.
> {{Quiet}}: read it as S3 does (tested 2026-10-31). The request is quiet if
> the text is {{true}} in any letter case, and not quiet for any other text; a
> {{Quiet}} value is never refused. Bind it as text and work out the flag
> before any key is deleted, so the NullPointerException cannot happen. This
> changes {{1}} and {{true}} with spaces around it from quiet to not quiet, as
> on S3.
> Why not the obvious alternatives (each measured):
> * Make JAXB fail on conversion errors. The converters raise no error for {{1
> 2}} or {{4294967297}}; they return 12 and 1. A strict JAXB handler would also
> refuse every element Ozone does not model, such as {{ChecksumCRC32}}, which
> S3's API defines for a {{Part}}.
> * A JAXB type adapter that throws. JAXB's default handler swallows the
> exception and leaves the field null.
> * A range check on the converted number. By then {{1 2}} and {{4294967297}}
> are already 12 and 1.
> h3. Testing
> The before-and-after results below are what the code predicts. The tests have
> not been written or run yet.
> * Unit, in {{TestCompleteMultipartUploadRequestUnmarshaller}} and
> {{TestMultipartUploadComplete}}: each form S3 was tested with gets S3's
> answer, and a refused request is not passed on to OM. Before the fix, {{1 2}}
> with part 12's ETag completes with part 12, and {{4294967297}} with part 1's
> ETag completes with part 1. {{01}}, {{+1}} and {{1}} with spaces around it
> still complete.
> * Unit, in {{TestMultiDeleteRequestUnmarshaller}} and
> {{TestObjectMultiDelete}}: each {{Quiet}} value S3 was tested with gets S3's
> answer. {{<Quiet>True</Quiet>}} deletes the key and answers in quiet mode;
> {{<Quiet>1</Quiet>}} and {{<Quiet>yes</Quiet>}} delete it and answer in
> verbose mode. Before the fix, {{True}} and {{yes}} delete the key and then
> throw a NullPointerException, and {{1}} answers in quiet mode. A request
> without {{Quiet}} still answers in verbose mode.
> * Unit: a check over the five request classes that fails if any field is
> bound to something other than text, so a future numeric or boolean field
> cannot bring this back.
> * Integration, in {{AbstractS3SDKV1Tests}}: complete an upload through a
> presigned URL with {{<PartNumber>4294967297</PartNumber>}}. The suite already
> sends a hand-written completion body this way. On master this returns 200.
> After the fix it returns 400 {{MalformedXML}} and no object is created.
> ----
> 🤖 Generated with [Claude Code|https://claude.com/claude-code]
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]