[ 
https://issues.apache.org/jira/browse/HDDS-17272?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Siyao Meng updated HDDS-17272:
------------------------------
    Affects Version/s: 2.2.1
                       2.2.0

> Interrupted S3 PUT to an encrypted bucket fails but commits an empty object 
> with the full body's ETag, replacing the previous version
> -------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-17272
>                 URL: https://issues.apache.org/jira/browse/HDDS-17272
>             Project: Apache Ozone
>          Issue Type: Bug
>    Affects Versions: 2.2.0, 2.2.1
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: MC-4-flush-encrypted-s3-write-before-commit.patch, 
> TestBugMC4TdeInterruptedPutCommit.java
>
>
> h3. Mechanism
> For a bucket with an encryption key the S3 gateway writes through 
> {{OzoneOutputStream(CryptoOutputStream(KeyOutputStream))}}. 
> {{CryptoOutputStream}} keeps up to its buffer size (8 KiB) of data and writes 
> it to the {{KeyOutputStream}} only on flush or close. {{S3ObjectWriteGuard}} 
> counts the bytes {{CryptoOutputStream}} accepted, so after {{copyFrom}} it 
> sees the full length and no failure.
> If the request thread has been interrupted, the write of the buffered data at 
> close is rejected by {{KeyOutputStreamSemaphore.acquire}} with 
> {{InterruptedIOException}} ("Write aborted. Interrupted waiting for 
> KeyOutputStream semaphore"). Unlike the other write failures of 
> {{KeyOutputStream}}, this one does not mark the stream closed. Hadoop's 
> {{CryptoOutputStream.close}} closes the inner stream even when its flush 
> failed, so {{KeyOutputStream.closeInternal}} runs the gateway's pre-commits, 
> which pass, and calls {{commitKey}} with the bytes it actually received. The 
> ETag of the full body is already in the key metadata. The commit RPC returns 
> "Call interrupted" to the gateway, but OM applies it.
> This is a regression from HDDS-15193. Up to 2.1.x, 
> {{KeyOutputStream.closeInternal}} checked for S3 gateway writes 
> ({{atomicKeyCreation}}) that the bytes it received match the size given at 
> open, which blocked this commit. HDDS-15193 moved that check into the 
> endpoints, where it counts the bytes accepted by the outermost stream.
> h3. Trigger
> # PUT an object to a bucket with an encryption key (any size), it succeeds.
> # PUT a new version of the same key with a body smaller than 8 KiB. The 
> request thread is interrupted after the gateway read the last body byte and 
> before the close. Jetty's {{QueuedThreadPool.doStop}} interrupts pool threads 
> that are still busy after half of the stop timeout, so an S3 gateway shutdown 
> is a possible source. That was not run end to end; the reproduction sets the 
> interrupt flag at that point.
> # The PUT fails with an error.
> An interrupt that lands during the body read, the open key RPC or a datanode 
> write fails the request without a commit. With a 16,400 byte body (16,384 
> bytes already in the {{KeyOutputStream}}) the close failed writing the chunk 
> and nothing was committed, so I expect only bodies that are still entirely in 
> the crypto buffer to be affected. Unencrypted buckets are not affected: the 
> interrupted write fails inside {{copyFrom}} and the guard blocks the commit.
> h3. Impact
> The client gets an error, yet the key now has size 0 and the ETag (md5) of 
> the full new body, and the previous version is gone. A reader sees an empty 
> object whose ETag claims other content. Nothing repairs it; a retry with 
> {{If-None-Match: *}} would be rejected because the key exists (not run).
> h3. Reproduction
> [^TestBugMC4TdeInterruptedPutCommit.java] 
> ({{hadoop-ozone/integration-test-s3}}) runs one MiniOzoneCluster with a 
> {{JavaKeyStoreProvider}} as KMS and drives the real {{ObjectEndpoint.put}} 
> and Ozone client; only the HTTP request is mocked, and the request body sets 
> the interrupt flag of the request thread once its last byte is read. On the 
> unmodified source both tests pass, which means the defect is present: on the 
> encrypted bucket the failed PUT leaves {{dataSize=0}} with the ETag of the 
> new 16 byte body, on the unencrypted bucket the previous 45 byte version 
> stays.
> h3. Patch
> [^MC-4-flush-encrypted-s3-write-before-commit.patch], against 
> b10a7d427bdf83807a7e48a1be507d3b02d5a503. It also applies to master at 
> e0de543cbe9.
> {{S3ObjectWriteGuard.close}} flushes the stream first when it wraps a 
> {{CryptoOutputStream}}, records a flush failure as the transfer failure and 
> then closes as before, so the existing pre-commit refuses the commit and the 
> {{KeyOutputStream}} only cleans up. Unencrypted writes are unchanged. 
> Encrypted writes do no extra work, because {{CryptoOutputStream.close}} 
> flushes before closing anyway; the flush just happens before the commit 
> decision now.
> Covered by {{encryptedWriteFailureOnCloseBlocksCommit}} and 
> {{encryptedWriteCommitsAllContent}} in the existing 
> {{TestS3ObjectWriteGuard}}, using a real {{CryptoOutputStream}}. Without the 
> change the first one fails because the key is committed. With it all 
> s3gateway unit tests (1620) pass and checkstyle is clean. With the patch the 
> reproduction's encrypted case fails, because the previous version stays.
> Found by TLA+ model checking and code review of the S3 gateway object put and 
> key commit paths under HDDS-15926, on commit 
> b10a7d427bdf83807a7e48a1be507d3b02d5a503. Checked against HDDS issues and 
> apache/ozone pull requests for duplicates before filing. The attached patch 
> is a proposal for review. Generated with Specula (Claude Opus 5.5).



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to