[
https://issues.apache.org/jira/browse/HDDS-17354?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Siyao Meng updated HDDS-17354:
------------------------------
Affects Version/s: 2.2.1
2.0.0
1.4.0
2.1.2
2.1.0
1.4.1
2.1.1
1.3.0
2.2.0
> Interrupted RocksDatabase close frees the native DB while an iterator still
> holds it, crashing the JVM on the next iterator call
> --------------------------------------------------------------------------------------------------------------------------------
>
> Key: HDDS-17354
> URL: https://issues.apache.org/jira/browse/HDDS-17354
> Project: Apache Ozone
> Issue Type: Bug
> Affects Versions: 1.3.0, 1.4.0, 2.0.0, 1.4.1, 2.1.0, 2.2.0, 2.1.1, 2.1.2,
> 2.2.1
> Reporter: Siyao Meng
> Priority: Minor
> Attachments: MC-1-defer-free-on-interrupted-rocksdb-close.patch
>
>
> h3. Mechanism
> {{RocksDatabase.waitAndClose}} waits until the reference counter drops to
> zero before it frees the column family handles, the DB and the options. Since
> HDDS-14800 every iterator opened through {{RocksDatabase}} holds such a
> reference, and the {{ManagedRocksIterator}} javadoc states that the DB cannot
> be destroyed while the iterator is open. When the waiting thread is
> interrupted, or enters the wait with its interrupt flag already set, the
> {{InterruptedException}} branch frees all of these at once, whatever the
> counter says, and returns. The thread that holds the iterator then calls
> {{next}} or {{seek}} on freed native memory. Only {{hasNext}} checks whether
> the DB is closed.
> The branch dates from HDDS-7284, which added the counter. Before HDDS-14800
> it only affected point operations in flight; since then any open iterator
> keeps the window open.
> h3. Trigger
> # A thread holds an open iterator on a store (for example a
> {{BlockDeletingService}} task iterating a datanode DB).
> # Another thread calls {{RDBStore.close}}, which waits in {{waitAndClose}}
> because of that reference.
> # The closing thread is interrupted, or already carries an interrupt. The
> sources I found are all in stop paths:
> {{ShutdownHookManager.executeShutdown}} cancels a stop hook that runs past
> {{ozone.service.shutdown.timeout}} (default 60s) with {{cancel(true)}}, and
> stop code such as {{BackgroundService.shutdown}} or the
> {{readExecutors.awaitTermination}} catch in {{OzoneContainer.stop}} re-sets
> the interrupt flag before later steps close DBs. On the datanode,
> {{volumeSet.shutdown}} closes the per volume DBs before
> {{blockDeletingService.shutdown}}.
> # The holder's next native iterator call crashes the JVM.
> This reading of the production triggers is argued from the code; I did not
> run a datanode or OM stop. I found no interrupt source that reaches a DB
> close in a process that keeps running.
> h3. Impact
> The JVM dies in native code (SIGSEGV or SIGBUS, with an hs_err file; the
> signal and frame vary, since this is a use after free) instead of finishing
> its shutdown, and remaining shutdown hooks may not run. Between the free and
> the crash the holder can return data read from freed memory. RocksDB recovers
> from its WAL on restart; what an unfixed crash during an in-flight write
> leaves on disk was not exercised.
> h3. Reproduction
> [^TestBugMC1InterruptedCloseFreesDbUnderIterator.java]
> ({{hadoop-hdds/framework}}) uses only {{RDBStore}} and {{RDBTable}} and
> interrupts the closer the way {{ShutdownHookManager}} does ({{submit}},
> {{get}} with a timeout, {{cancel(true)}}). Both tests assert the correct
> behaviour, so they fail on unfixed code. The first shows that after the
> interrupted {{close}} returns, the native DB no longer owns its handle
> although the iterator is still open. The second runs the same sequence in a
> child JVM, which reads one entry and then dies in native code (SIGSEGV or
> SIGBUS, with an hs_err file) on the following {{seek}}. With the patch both
> pass: the child reads, seeks and exits 0.
> h3. Patch
> [^MC-1-defer-free-on-interrupted-rocksdb-close.patch], against
> bf3829e6c8ac69ae17585585703f91426dcdf6b3. It also applies to master at
> 4fb68e96831.
> On interrupt, {{waitAndClose}} no longer frees anything. It logs one warning
> naming the DB and saying that it keeps its LOCK until the open references
> close, starts a {{DBCloser}} thread that runs the same wait (the path the
> asynchronous close after a RocksDB error already uses), re-sets the interrupt
> flag and returns as early as before. The native DB is freed when the last
> reference is released. No caller waits longer than today, so this does not
> add to the stall described in HDDS-17020. The only behaviour change is on the
> interrupted path with references held: the DB and its LOCK file are released
> later, when the holder finishes.
> Covered by {{testInterruptedCloseDoesNotFreeDBUnderOpenIterator}} in the
> existing {{TestRDBStoreIteratorWithDBClose}}, which closes the store with the
> interrupt flag set while an iterator is open and asserts that the interrupt
> status is kept, the native DB stays alive until the iterator is closed, and
> is freed afterwards. It fails without the change. With it the framework DB
> suites (104 tests) and the container service suites that close DBs
> ({{TestContainerCache}}, {{TestHddsVolume}}, {{TestVolumeSetDiskChecks}},
> {{TestStorageVolumeChecker}}, {{TestDatanodeStoreCache}}, 51 tests) pass,
> checkstyle is clean.
> Found by TLA+ model checking and code review of the RocksDB handle close and
> shutdown paths under HDDS-15926, on commit
> bf3829e6c8ac69ae17585585703f91426dcdf6b3. 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
(v10.3.26#10030026)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]