Siyao Meng created HDDS-17354:
---------------------------------
Summary: 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
Reporter: Siyao Meng
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]