CVE-2026-117962
Overview
Background
- Quota Manager encrypted streams
dom/quota/DecryptingInputStreamwraps a basensIInputStreamand decrypts fixed-size blocks on the fly. It implementsnsIInputStream,nsISeekableStream,nsITellableStream,nsICloneableInputStream, andnsIIPCSerializableInputStream.- Mutable decryption cursor
- State such as
mPlainBuffer(decrypted bytes),mPlainBytes(count),mNextByte(read cursor),mEncryptedBlock,mCipherStrategy, andmBaseStreamtogether track where in the stream reading is.ReadSegmentsconsumes from this cursor;Seekrewinds/moves it. - Thread-safe refcount, no data lock
- The stream is reference-counted thread-safely, so it can be shared across threads. As the patch comment states, an HTTP upload stream can be read on the socket thread while a redirect rewinds it (
Seek) on the main thread. - `MOZ_GUARDED_BY` / `MOZ_REQUIRES` / `Mutex`
- Mozilla’s static thread-safety annotations and a
Mutextype; a field annotatedMOZ_GUARDED_BY(mMutex)must only be touched withmMutexheld, and a method annotatedMOZ_REQUIRES(mMutex)must be called with it held.
Root Cause Analysis
DecryptingInputStream’s mutable decryption state had no lock, yet the object is thread-safely refcounted and, in the HTTP upload case, is read on the socket thread while being seeked on the main thread on redirect. ReadSegments/ReadAll/ParseNextChunk advance mNextByte/mPlainBytes and refill mPlainBuffer/mEncryptedBlock using mCipherStrategy, while a concurrent Seek resets those same fields. With no serialization this is a data race: a read can copy from mPlainBuffer using a mNextByte/mPlainBytes pair that a concurrent Seek has partially updated, producing a torn read — reading from a stale cursor with a stale count — and operating on cipher/buffer state that is mid-mutation. The regression test DummyCipherStrategy_ConcurrentReadAndSeek demonstrates exactly this: a seek thread hammers Seek while the main thread Reads a byte-ramp payload, and asserts each returned chunk is consecutive; pre-patch the race yields non-consecutive (“torn”) bytes.
The patch adds a Mutex mMutex to DecryptingInputStreamBase, annotates all mutable cursor/cipher fields with MOZ_GUARDED_BY(mMutex), and takes MutexAutoLock lock(mMutex) at the start of every public entry point that touches that state: the constructors (via Init, now MOZ_REQUIRES(mMutex)), Close, Available, StreamStatus, BaseStreamStatus, ReadSegments, Tell, Seek, Clone, Serialize, and Deserialize. TellInternal is demoted from NS_IMETHODIMP to a private nsresult ... MOZ_REQUIRES(mMutex) helper and Available/Tell now call TellInternal while already holding the lock (avoiding a self-deadlock a re-entrant Tell would cause). The nsICloneableInputStream/nsIIPCSerializableInputStream conditional interface-map entries change their guard from !mBaseStream to !mBlockSize, because mBaseStream is now lock-guarded and must not be read in QueryInterface without the lock, whereas mBlockSize is effectively immutable after construction. Net effect: all reads and seeks of the decryption cursor are now mutually exclusive.
Note on advisory wording: the advisory calls this a “Sandbox escape due to incorrect boundary conditions in the Storage: Quota Manager component.” The diff does not substantiate boundary/off-by-one logic or a sandbox-escape mechanism; it is unambiguously a missing-lock data race on shared stream state. Trusting the diff, the true class is a concurrency/race bug; any memory-corruption or escape consequence is inferred, not shown.
Read and Seek raced and produced torn reads from mid-mutation state.Attack Path
- Shared stream
Content triggers an operation that serializes an encrypted quota-backed stream as an HTTP upload body, so the
DecryptingInputStreamis read on the socket thread (inferred reachability from the patch’s own description of the HTTP-upload/redirect scenario). - Concurrent rewind
A redirect causes the upload stream to be rewound via
Seekon the main thread while the socket thread is mid-ReadSegments. - Race window
mNextByte/mPlainBytes/mPlainBuffer/mCipherStrategyare mutated bySeekwhileReadSegmentsreads them, with no lock. - Torn/stale read
ReadSegmentscopies using a stale cursor/count, yielding corrupted or wrongly-bounded plaintext (the test catches this as non-consecutive bytes). - Primitive Data corruption of stream contents and operation on inconsistent buffer/cipher state; memory-safety corruption or any sandbox consequence beyond this is inferred and not demonstrated by the diff.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
mKeydom/quota/DecryptingInputStream_impl.h |
modified | |
ifdom/quota/DecryptingInputStream_impl.h |
modified | |
TEST_Fdom/quota/test/gtest/TestEncryptedStream.cpp |
modified | |
fordom/quota/test/gtest/TestEncryptedStream.cpp |
modified | |
whiledom/quota/test/gtest/TestEncryptedStream.cpp |
modified |
Files Changed
dom/quota/DecryptingInputStream.cppdom/quota/DecryptingInputStream.hdom/quota/DecryptingInputStream_impl.hdom/quota/test/gtest/TestEncryptedStream.cpp
Audit Directions
- Other refcounted streams shared across threadsAudit other
nsIInputStream/nsISeekableStreamimplementations indom/quotaandnetwerkthat keep a mutable read cursor but no lock yet can be read on the socket thread while seeked on the main thread. - Thread-safety annotation coverageUse
MOZ_GUARDED_BY/MOZ_REQUIRESto find mutable members of thread-safely-refcounted classes that lack guards, the exact pattern this patch retrofits. - QueryInterface reading guarded stateReview conditional interface maps that test lock-guarded members (like the old
!mBaseStream); prefer testing immutable fields (!mBlockSize) to avoid unlocked reads.
Patch
diff --git a/dom/quota/DecryptingInputStream.cpp b/dom/quota/DecryptingInputStream.cpp
index 9bb867d1db2..870b34b93a3 100644
--- a/dom/quota/DecryptingInputStream.cpp
+++ b/dom/quota/DecryptingInputStream.cpp
@@ -16,15 +16,16 @@ NS_INTERFACE_MAP_BEGIN(DecryptingInputStreamBase)
NS_INTERFACE_MAP_ENTRY(nsIInputStream)
NS_INTERFACE_MAP_ENTRY(nsISeekableStream)
NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsICloneableInputStream,
- mBaseCloneableInputStream || !mBaseStream)
+ mBaseCloneableInputStream || !mBlockSize)
NS_INTERFACE_MAP_ENTRY_CONDITIONAL(
nsIIPCSerializableInputStream,
- mBaseIPCSerializableInputStream || !mBaseStream)
+ mBaseIPCSerializableInputStream || !mBlockSize)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream)
NS_INTERFACE_MAP_END
DecryptingInputStreamBase::DecryptingInputStreamBase(
MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream, size_t aBlockSize) {
+ MutexAutoLock lock(mMutex);
Init(std::move(aBaseStream), aBlockSize);
}
diff --git a/dom/quota/DecryptingInputStream.h b/dom/quota/DecryptingInputStream.h
index 62bb46ae29a..1c64e6875d6 100644
--- a/dom/quota/DecryptingInputStream.h
+++ b/dom/quota/DecryptingInputStream.h
@@ -15,6 +15,7 @@
#include "ErrorList.h"
#include "mozilla/InitializedOnce.h"
#include "mozilla/Maybe.h"
+#include "mozilla/Mutex.h"
#include "mozilla/NotNull.h"
#include "mozilla/ipc/InputStreamParams.h"
#include "nsCOMPtr.h"
@@ -61,16 +62,22 @@ class DecryptingInputStreamBase : public nsIInputStream,
virtual ~DecryptingInputStreamBase() = default;
void Init(MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream,
- size_t aBlockSize);
+ size_t aBlockSize) MOZ_REQUIRES(mMutex);
// Convenience routine to determine how many bytes of plain data
// we currently have in our plain buffer.
- size_t PlainLength() const;
+ size_t PlainLength() const MOZ_REQUIRES(mMutex);
size_t EncryptedBufferLength() const;
+ // The stream is refcounted thread-safely and may be read on one thread while
+ // being seeked from another (e.g. an HTTP upload stream that is read on the
+ // socket thread and rewound on the main thread on redirect). All mutable
+ // state below is therefore only accessed with this lock held.
+ Mutex mMutex{"DecryptingInputStreamBase::mMutex"};
+
LazyInitializedOnceEarlyDestructible<const NotNull<nsCOMPtr<nsIInputStream>>>
- mBaseStream;
+ mBaseStream MOZ_GUARDED_BY(mMutex);
LazyInitializedOnce<const NotNull<nsISeekableStream*>> mBaseSeekableStream;
LazyInitializedOnce<const NotNull<nsICloneableInputStream*>>
mBaseCloneableInputStream;
@@ -78,10 +85,10 @@ class DecryptingInputStreamBase : public nsIInputStream,
mBaseIPCSerializableInputStream;
// Number of bytes of plain data in mPlainBuffer.
- size_t mPlainBytes = 0;
+ size_t mPlainBytes MOZ_GUARDED_BY(mMutex) = 0;
// Next byte of mPlainBuffer to return in ReadSegments().
- size_t mNextByte = 0;
+ size_t mNextByte MOZ_GUARDED_BY(mMutex) = 0;
LazyInitializedOnceNotNull<const size_t> mBlockSize;
};
@@ -111,7 +118,6 @@ class DecryptingInputStream final : public DecryptingInputStreamBase {
uint32_t aCount, uint32_t* _retval) override;
NS_DECL_NSITELLABLESTREAM
- NS_IMETHOD TellInternal(int64_t* const aRetval, uint64_t const aBlockOffset);
NS_IMETHOD Seek(int32_t aWhence, int64_t aOffset) override;
@@ -125,9 +131,13 @@ class DecryptingInputStream final : public DecryptingInputStreamBase {
private:
~DecryptingInputStream();
+ nsresult TellInternal(int64_t* aRetval, uint64_t aBlockOffset)
+ MOZ_REQUIRES(mMutex);
+
// Parse the next chunk of data. This populates mPlainBuffer (until the
// stream position is at EOF).
- nsresult ParseNextChunk(bool aCheckAvailableBytes, uint32_t* aBytesReadOut);
+ nsresult ParseNextChunk(bool aCheckAvailableBytes, uint32_t* aBytesReadOut)
+ MOZ_REQUIRES(mMutex);
// Convenience routine to Read() from the base stream until we get
// the given number of bytes or reach EOF.
@@ -146,25 +156,27 @@ class DecryptingInputStream final : public DecryptingInputStreamBase {
// aBytesReadOut - An out parameter indicating how many bytes were
// read.
nsresult ReadAll(char* aBuf, uint32_t aCount, uint32_t aMinValidCount,
- bool aCheckAvailableBytes, uint32_t* aBytesReadOut);
+ bool aCheckAvailableBytes, uint32_t* aBytesReadOut)
+ MOZ_REQUIRES(mMutex);
- bool EnsureBuffers();
+ bool EnsureBuffers() MOZ_REQUIRES(mMutex);
- nsresult EnsureDecryptedStreamSize();
+ nsresult EnsureDecryptedStreamSize() MOZ_REQUIRES(mMutex);
- CipherStrategy mCipherStrategy;
+ CipherStrategy mCipherStrategy MOZ_GUARDED_BY(mMutex);
LazyInitializedOnce<const typename CipherStrategy::KeyType> mKey;
// Buffer to hold encrypted data. Must copy here since we need a
// flat buffer to run the decryption process on.
using EncryptedBlockType = EncryptedBlock<CipherStrategy::BlockPrefixLength,
CipherStrategy::BasicBlockSize>;
- Maybe<EncryptedBlockType> mEncryptedBlock;
+ Maybe<EncryptedBlockType> mEncryptedBlock MOZ_GUARDED_BY(mMutex);
// Buffer storing the resulting plain data.
- nsTArray<uint8_t> mPlainBuffer;
+ nsTArray<uint8_t> mPlainBuffer MOZ_GUARDED_BY(mMutex);
- LazyInitializedOnce<const int64_t> mDecryptedStreamSize;
+ LazyInitializedOnce<const int64_t> mDecryptedStreamSize
+ MOZ_GUARDED_BY(mMutex);
};
} // namespace mozilla::dom::quota
diff --git a/dom/quota/DecryptingInputStream_impl.h b/dom/quota/DecryptingInputStream_impl.h
index 384368eea6e..e7a30e3d352 100644
--- a/dom/quota/DecryptingInputStream_impl.h
+++ b/dom/quota/DecryptingInputStream_impl.h
@@ -31,6 +31,7 @@ DecryptingInputStream<CipherStrategy>::DecryptingInputStream(
typename CipherStrategy::KeyType aKey)
: DecryptingInputStreamBase(std::move(aBaseStream), aBlockSize),
mKey(aKey) {
+ MutexAutoLock lock(mMutex);
// XXX Move this to a fallible init function.
MOZ_ALWAYS_SUCCEEDS(mCipherStrategy.Init(CipherMode::Decrypt,
CipherStrategy::SerializeKey(aKey)));
@@ -53,6 +54,7 @@ DecryptingInputStream<CipherStrategy>::DecryptingInputStream()
template <typename CipherStrategy>
NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Close() {
+ MutexAutoLock lock(mMutex);
if (!mBaseStream) {
return NS_OK;
}
@@ -69,12 +71,13 @@ NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Close() {
template <typename CipherStrategy>
NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Available(
uint64_t* aLengthOut) {
+ MutexAutoLock lock(mMutex);
if (!mBaseStream) {
return NS_BASE_STREAM_CLOSED;
}
int64_t current;
- nsresult rv = Tell(¤t);
+ nsresult rv = TellInternal(¤t, mNextByte);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
@@ -94,11 +97,13 @@ NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Available(
template <typename CipherStrategy>
NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::StreamStatus() {
+ MutexAutoLock lock(mMutex);
return mBaseStream ? NS_OK : NS_BASE_STREAM_CLOSED;
}
template <typename CipherStrategy>
nsresult DecryptingInputStream<CipherStrategy>::BaseStreamStatus() {
+ MutexAutoLock lock(mMutex);
return mBaseStream ? (*mBaseStream)->StreamStatus() : NS_BASE_STREAM_CLOSED;
}
@@ -108,6 +113,7 @@ NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::ReadSegments(
uint32_t* aBytesReadOut) {
*aBytesReadOut = 0;
+ MutexAutoLock lock(mMutex);
if (!mBaseStream) {
return NS_BASE_STREAM_CLOSED;
}
@@ -370,11 +376,12 @@ nsresult DecryptingInputStream<CipherStrategy>::EnsureDecryptedStreamSize() {
template <typename CipherStrategy>
NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Tell(
int64_t* const aRetval) {
+ MutexAutoLock lock(mMutex);
return TellInternal(aRetval, mNextByte);
}
template <typename CipherStrategy>
-NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::TellInternal(
+nsresult DecryptingInputStream<CipherStrategy>::TellInternal(
int64_t* const aRetval, uint64_t const aBlockOffset) {
MOZ_ASSERT(aRetval);
@@ -410,6 +417,7 @@ NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::TellInternal(
template <typename CipherStrategy>
NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Seek(const int32_t aWhence,
int64_t aOffset) {
+ MutexAutoLock lock(mMutex);
if (!mBaseStream) {
return NS_BASE_STREAM_CLOSED;
}
@@ -549,6 +557,7 @@ NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Seek(const int32_t aWhence,
template <typename CipherStrategy>
NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Clone(
nsIInputStream** _retval) {
+ MutexAutoLock lock(mMutex);
if (!mBaseStream) {
return NS_BASE_STREAM_CLOSED;
}
@@ -575,6 +584,7 @@ template <typename CipherStrategy>
void DecryptingInputStream<CipherStrategy>::Serialize(
mozilla::ipc::InputStreamParams& aParams, uint32_t aMaxSize,
uint32_t* aSizeUsed) {
+ MutexAutoLock lock(mMutex);
MOZ_ASSERT(mBaseStream);
MOZ_ASSERT(mBaseIPCSerializableInputStream);
@@ -602,6 +612,7 @@ bool DecryptingInputStream<CipherStrategy>::Deserialize(
return false;
}
+ MutexAutoLock lock(mMutex);
Init(WrapNotNull<nsCOMPtr<nsIInputStream>>(std::move(stream)),
params.blockSize());
diff --git a/dom/quota/test/gtest/TestEncryptedStream.cpp b/dom/quota/test/gtest/TestEncryptedStream.cpp
index a34225aefb3..1a105dbbb80 100644
--- a/dom/quota/test/gtest/TestEncryptedStream.cpp
+++ b/dom/quota/test/gtest/TestEncryptedStream.cpp
@@ -4,6 +4,7 @@
#include <algorithm>
#include <cstdint>
+#include <iterator>
#include <new>
#include <numeric>
#include <string>
@@ -14,6 +15,7 @@
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "mozilla/Assertions.h"
+#include "mozilla/Atomics.h"
#include "mozilla/Attributes.h"
#include "mozilla/FixedBufferOutputStream.h"
#include "mozilla/NotNull.h"
@@ -34,10 +36,12 @@
#include "nsISeekableStream.h"
#include "nsISupports.h"
#include "nsITellableStream.h"
+#include "nsIThread.h"
#include "nsStreamUtils.h"
#include "nsString.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
+#include "nsThreadUtils.h"
#include "nscore.h"
#include "nss.h"
@@ -665,6 +669,74 @@ TEST_P(ParametrizedCryptTest, zeroInitializedEncryptedBlock) {
EXPECT_THAT(unusedBytesInFirstBlock, testing::Each(0ul));
}
+// Regression test for bug 2072411: an HTTP upload stream is read on the socket
+// thread while a redirect rewinds it on the main thread. Read and Seek must
+// serialize, otherwise ReadSegments copies from a stale cursor with a stale
+// count. The test data is a byte ramp, so a torn read shows up as a
+// non-consecutive chunk even when it does not crash.
+TEST_F(DOM_Quota_EncryptedStream, DummyCipherStrategy_ConcurrentReadAndSeek) {
+ using CipherStrategy = DummyCipherStrategy;
+ constexpr size_t kBlockSize = 1024;
+ constexpr size_t kDataSize = 4000;
+ constexpr size_t kReadChunkSize = 64;
+ constexpr size_t kSeekIterations = 5000;
+
+ const auto baseOutputStream = WrapNotNull(
+ RefPtr<FixedBufferOutputStream>{FixedBufferOutputStream::Create(8192)});
+
+ const auto data = MakeTestData(kDataSize);
+
+ WriteTestData<CipherStrategy>(
+ nsCOMPtr<nsIOutputStream>{baseOutputStream.get()}, Span{data}, kDataSize,
+ kBlockSize, CipherStrategy::KeyType{}, FlushMode::Never);
+
+ const auto baseInputStream =
+ MakeRefPtr<ArrayBufferInputStream>(baseOutputStream->WrittenData());
+
+ const auto inStream = MakeRefPtr<DecryptingInputStream<CipherStrategy>>(
+ WrapNotNull(nsCOMPtr<nsIInputStream>{baseInputStream}), kBlockSize,
+ CipherStrategy::KeyType{});
+
+ Atomic<bool> done{false};
+ Atomic<uint32_t> seekFailures{0};
+
+ nsCOMPtr<nsIThread> seekThread;
+ ASSERT_EQ(NS_OK, NS_NewNamedThread(
+ "SeekThread", getter_AddRefs(seekThread),
+ NS_NewRunnableFunction(
+ "DOM_Quota_EncryptedStream::ConcurrentSeek", [&] {
+ constexpr int64_t kOffsets[] = {0, kDataSize - 48,
+ kDataSize / 2,
+ kDataSize - 1};
+ for (size_t i = 0; i < kSeekIterations; ++i) {
+ if (NS_FAILED(inStream->Seek(
+ nsISeekableStream::NS_SEEK_SET,
+ kOffsets[i % std::size(kOffsets)]))) {
+ ++seekFailures;
+ }
+ }
+ done = true;
+ })));
+
+ uint8_t buffer[kReadChunkSize];
+ size_t reads = 0;
+ while (!done) {
+ uint32_t read = 0;
+ ASSERT_EQ(NS_OK, inStream->Read(reinterpret_cast<char*>(buffer),
+ kReadChunkSize, &read));
+ ASSERT_LE(read, kReadChunkSize);
+ for (uint32_t j = 1; j < read; ++j) {
+ ASSERT_EQ(static_cast<uint8_t>(buffer[0] + j), buffer[j])
+ << "torn read at read " << reads << ", byte " << j;
+ }
+ ++reads;
+ }
+
+ EXPECT_EQ(NS_OK, seekThread->Shutdown());
+ EXPECT_EQ(0u, static_cast<uint32_t>(seekFailures));
+ EXPECT_GT(reads, 0u);
+}
+
enum struct SeekOffset {
Zero,
MinusHalfDataSize,
Regression Test / PoC
diff --git a/dom/quota/test/gtest/TestEncryptedStream.cpp b/dom/quota/test/gtest/TestEncryptedStream.cpp
index a34225aefb3..1a105dbbb80 100644
--- a/dom/quota/test/gtest/TestEncryptedStream.cpp
+++ b/dom/quota/test/gtest/TestEncryptedStream.cpp
@@ -4,6 +4,7 @@
#include <algorithm>
#include <cstdint>
+#include <iterator>
#include <new>
#include <numeric>
#include <string>
@@ -14,6 +15,7 @@
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "mozilla/Assertions.h"
+#include "mozilla/Atomics.h"
#include "mozilla/Attributes.h"
#include "mozilla/FixedBufferOutputStream.h"
#include "mozilla/NotNull.h"
@@ -34,10 +36,12 @@
#include "nsISeekableStream.h"
#include "nsISupports.h"
#include "nsITellableStream.h"
+#include "nsIThread.h"
#include "nsStreamUtils.h"
#include "nsString.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
+#include "nsThreadUtils.h"
#include "nscore.h"
#include "nss.h"
@@ -665,6 +669,74 @@ TEST_P(ParametrizedCryptTest, zeroInitializedEncryptedBlock) {
EXPECT_THAT(unusedBytesInFirstBlock, testing::Each(0ul));
}
+// Regression test for bug 2072411: an HTTP upload stream is read on the socket
+// thread while a redirect rewinds it on the main thread. Read and Seek must
+// serialize, otherwise ReadSegments copies from a stale cursor with a stale
+// count. The test data is a byte ramp, so a torn read shows up as a
+// non-consecutive chunk even when it does not crash.
+TEST_F(DOM_Quota_EncryptedStream, DummyCipherStrategy_ConcurrentReadAndSeek) {
+ using CipherStrategy = DummyCipherStrategy;
+ constexpr size_t kBlockSize = 1024;
+ constexpr size_t kDataSize = 4000;
+ constexpr size_t kReadChunkSize = 64;
+ constexpr size_t kSeekIterations = 5000;
+
+ const auto baseOutputStream = WrapNotNull(
+ RefPtr<FixedBufferOutputStream>{FixedBufferOutputStream::Create(8192)});
+
+ const auto data = MakeTestData(kDataSize);
+
+ WriteTestData<CipherStrategy>(
+ nsCOMPtr<nsIOutputStream>{baseOutputStream.get()}, Span{data}, kDataSize,
+ kBlockSize, CipherStrategy::KeyType{}, FlushMode::Never);
+
+ const auto baseInputStream =
+ MakeRefPtr<ArrayBufferInputStream>(baseOutputStream->WrittenData());
+
+ const auto inStream = MakeRefPtr<DecryptingInputStream<CipherStrategy>>(
+ WrapNotNull(nsCOMPtr<nsIInputStream>{baseInputStream}), kBlockSize,
+ CipherStrategy::KeyType{});
+
+ Atomic<bool> done{false};
+ Atomic<uint32_t> seekFailures{0};
+
+ nsCOMPtr<nsIThread> seekThread;
+ ASSERT_EQ(NS_OK, NS_NewNamedThread(
+ "SeekThread", getter_AddRefs(seekThread),
+ NS_NewRunnableFunction(
+ "DOM_Quota_EncryptedStream::ConcurrentSeek", [&] {
+ constexpr int64_t kOffsets[] = {0, kDataSize - 48,
+ kDataSize / 2,
+ kDataSize - 1};
+ for (size_t i = 0; i < kSeekIterations; ++i) {
+ if (NS_FAILED(inStream->Seek(
+ nsISeekableStream::NS_SEEK_SET,
+ kOffsets[i % std::size(kOffsets)]))) {
+ ++seekFailures;
+ }
+ }
+ done = true;
+ })));
+
+ uint8_t buffer[kReadChunkSize];
+ size_t reads = 0;
+ while (!done) {
+ uint32_t read = 0;
+ ASSERT_EQ(NS_OK, inStream->Read(reinterpret_cast<char*>(buffer),
+ kReadChunkSize, &read));
+ ASSERT_LE(read, kReadChunkSize);
+ for (uint32_t j = 1; j < read; ++j) {
+ ASSERT_EQ(static_cast<uint8_t>(buffer[0] + j), buffer[j])
+ << "torn read at read " << reads << ", byte " << j;
+ }
+ ++reads;
+ }
+
+ EXPECT_EQ(NS_OK, seekThread->Shutdown());
+ EXPECT_EQ(0u, static_cast<uint32_t>(seekFailures));
+ EXPECT_GT(reads, 0u);
+}
+
enum struct SeekOffset {
Zero,
MinusHalfDataSize,