diff --git a/docs/API.md b/docs/API.md
index c8e2541f..7cf54ba3 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -327,6 +327,39 @@ await writer.WriteAllAsync("C:\\source", "*", SearchOption.AllDirectories, cance
## Common Options
+### Decompression dictionary limits
+
+`SharpCompress.Common.DictionarySizeLimits` contains process-wide static size limits
+in bytes. Set them once at application startup, not around individual requests:
+
+```csharp
+// Opt in only when trusted archives require larger dictionaries.
+DictionarySizeLimits.Rar5 = 256L << 20;
+```
+
+| Static property | Default maximum |
+| --- | --- |
+| `Rar4` | 4 MiB (the RAR4 format maximum) |
+| `Rar5` | 64 MiB |
+| `Lzma` (raw LZMA, LZip, ZIP, 7z) | 64 MiB |
+| `Lzma2` (XZ, 7z) | 64 MiB |
+| `Ppmd` (ZIP, 7z, RAR models) | 64 MiB |
+| `ZStandard` (streaming windows) | 128 MiB |
+
+Limits are read directly by the shared codecs before dictionary/model allocation,
+so Archive, Reader, direct decoder, sync, and async paths use the same settings.
+No reader options or decoder overloads are needed. Settings require positive values;
+invalid assignments throw `ArgumentOutOfRangeException` without changing the setting.
+Oversized dictionaries throw `InvalidFormatException`; Zstandard reports
+`ZstdException` with `ZSTD_error_frameParameter_windowTooLarge`. Zstandard's minimum
+configurable window limit is 1 KiB. Format and engine restrictions still apply.
+
+These are dictionary/window or model limits, not total-memory, output-size, or
+concurrency limits. Pool rounding and allocator overhead can increase actual memory.
+Shared PPMd allocators also apply the model limit during compression. Fixed/bounded
+codecs retain their format-defined bounds. Custom providers must enforce their own
+limits. Zstandard's one-shot `Unwrap` APIs retain their existing output-size bounds.
+
### ReaderOptions
Use preset properties and fluent helpers for common configurations:
diff --git a/docs/USAGE.md b/docs/USAGE.md
index 0063c175..f419d264 100644
--- a/docs/USAGE.md
+++ b/docs/USAGE.md
@@ -483,3 +483,15 @@ ZIP Shrink output buffering grows with decoded output rather than the declared u
7z header counts and coder property lengths are validated against their backing metadata before allocation. File counts are checked against parsed substreams and empty-stream bitmaps, including external metadata. Encoded headers and additional metadata are buffered incrementally as the decoder produces bytes, rather than preallocating the declared unpacked size.
LZMA/LZMA2 dictionary storage starts small and grows with decoded history, up to the supported dictionary size. These safeguards avoid allocations based solely on forged sizes; they do not impose an overall decompressed-output or process-memory limit.
+
+Shared codecs also check `SharpCompress.Common.DictionarySizeLimits` before
+allocating dictionaries/windows or PPMd models. Defaults are 4 MiB for RAR4,
+64 MiB for RAR5, LZMA (including LZip), LZMA2 (including XZ), and PPMd, and
+128 MiB for Zstandard streaming windows. A tiny RAR5 entry therefore cannot rent
+a 256 MiB or 1 GiB window under the default settings.
+
+These settings are **process-wide**, not per-reader options. Configure them once
+at startup if trusted input requires larger dictionaries, for example
+`DictionarySizeLimits.Rar5 = 256L << 20;`. Do not temporarily raise them around
+untrusted requests. Bound extraction output and concurrency separately; dictionary
+limits are not a total-memory budget. See the [API reference](API.md#decompression-dictionary-limits).
diff --git a/src/SharpCompress/Common/DictionarySizeLimits.cs b/src/SharpCompress/Common/DictionarySizeLimits.cs
new file mode 100644
index 00000000..1ea21d62
--- /dev/null
+++ b/src/SharpCompress/Common/DictionarySizeLimits.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Threading;
+
+namespace SharpCompress.Common;
+
+///
+/// Process-wide dictionary/window and PPMd model size limits, in bytes.
+/// Configure once at application startup, not for individual extraction requests.
+/// These limits do not bound total memory, allocator overhead, or decompressed output.
+///
+public static class DictionarySizeLimits
+{
+ private static long rar4 = 4L << 20;
+ private static long rar5 = 64L << 20;
+ private static long lzma = 64L << 20;
+ private static long lzma2 = 64L << 20;
+ private static long ppmd = 64L << 20;
+ private static long zStandard = 128L << 20;
+
+ /// RAR4 window limit. Defaults to the format maximum of 4 MiB.
+ public static long Rar4
+ {
+ get => Interlocked.Read(ref rar4);
+ set => SetMaximum(ref rar4, value, nameof(Rar4));
+ }
+
+ /// RAR5 window limit. Defaults to 64 MiB.
+ public static long Rar5
+ {
+ get => Interlocked.Read(ref rar5);
+ set => SetMaximum(ref rar5, value, nameof(Rar5));
+ }
+
+ /// LZMA dictionary limit, including LZip, ZIP, and 7z. Defaults to 64 MiB.
+ public static long Lzma
+ {
+ get => Interlocked.Read(ref lzma);
+ set => SetMaximum(ref lzma, value, nameof(Lzma));
+ }
+
+ /// LZMA2 dictionary limit, including XZ and 7z. Defaults to 64 MiB.
+ public static long Lzma2
+ {
+ get => Interlocked.Read(ref lzma2);
+ set => SetMaximum(ref lzma2, value, nameof(Lzma2));
+ }
+
+ /// PPMd model limit, including ZIP, 7z, and RAR models. Defaults to 64 MiB.
+ public static long Ppmd
+ {
+ get => Interlocked.Read(ref ppmd);
+ set => SetMaximum(ref ppmd, value, nameof(Ppmd));
+ }
+
+ /// Zstandard streaming window limit. Defaults to 128 MiB.
+ public static long ZStandard
+ {
+ get => Interlocked.Read(ref zStandard);
+ set => SetMaximum(ref zStandard, value, nameof(ZStandard));
+ }
+
+ private static void SetMaximum(ref long field, long value, string name)
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(name, "Dictionary size limits must be positive.");
+ }
+ Interlocked.Exchange(ref field, value);
+ }
+
+ internal static void Validate(long size, long maximum, string format)
+ {
+ if (size <= 0 || size > maximum)
+ {
+ throw new InvalidFormatException(
+ $"{format} dictionary size {size} is invalid or exceeds the maximum of {maximum} bytes."
+ );
+ }
+ }
+}
diff --git a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs
index 02fd16d7..888af50a 100644
--- a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs
+++ b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs
@@ -4,6 +4,7 @@ using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading.Tasks;
+using SharpCompress.Common;
using SharpCompress.Compressors.LZMA.LZ;
using SharpCompress.Compressors.LZMA.RangeCoder;
@@ -229,6 +230,7 @@ public partial class Decoder : ICoder, ISetDecoderProperties, IDisposable
}
_outWindow = new OutWindow();
var blockSize = Math.Max(_dictionarySize, (1 << 12));
+ DictionarySizeLimits.Validate(blockSize, DictionarySizeLimits.Lzma, "LZMA");
_outWindow.Create(blockSize);
}
diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs
index 361378a6..09f4af31 100644
--- a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs
+++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs
@@ -4,6 +4,7 @@ using System.Buffers.Binary;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using SharpCompress.Common;
using SharpCompress.Compressors.LZMA.LZ;
using SharpCompress.IO;
@@ -54,6 +55,7 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable
if (!isLzma2)
{
_dictionarySize = BinaryPrimitives.ReadInt32LittleEndian(properties.AsSpan(1));
+ DictionarySizeLimits.Validate(_dictionarySize, DictionarySizeLimits.Lzma, "LZMA");
_decoder = new Decoder();
_decoder.SetDecoderProperties(properties);
Properties = properties;
@@ -72,6 +74,7 @@ public partial class LzmaStream : Stream, IStreamStack, IAsyncDisposable
}
_dictionarySize = 2 | (properties[0] & 1);
_dictionarySize <<= (properties[0] >> 1) + 11;
+ DictionarySizeLimits.Validate(_dictionarySize, DictionarySizeLimits.Lzma2, "LZMA2");
Properties = new byte[1];
_availableBytes = 0;
diff --git a/src/SharpCompress/Compressors/PPMd/H/SubAllocator.cs b/src/SharpCompress/Compressors/PPMd/H/SubAllocator.cs
index df6dfa60..8927ff50 100644
--- a/src/SharpCompress/Compressors/PPMd/H/SubAllocator.cs
+++ b/src/SharpCompress/Compressors/PPMd/H/SubAllocator.cs
@@ -3,6 +3,7 @@
using System;
using System.Buffers;
using System.Text;
+using SharpCompress.Common;
namespace SharpCompress.Compressors.PPMd.H;
@@ -155,6 +156,7 @@ internal class SubAllocator : IDisposable
public virtual bool StartSubAllocator(int saSize)
{
+ DictionarySizeLimits.Validate(saSize, DictionarySizeLimits.Ppmd, "PPMd");
var t = saSize;
if (_subAllocatorSize == t)
{
diff --git a/src/SharpCompress/Compressors/PPMd/I1/Allocator.cs b/src/SharpCompress/Compressors/PPMd/I1/Allocator.cs
index c4d8aa4b..0c8846d0 100644
--- a/src/SharpCompress/Compressors/PPMd/I1/Allocator.cs
+++ b/src/SharpCompress/Compressors/PPMd/I1/Allocator.cs
@@ -1,5 +1,7 @@
#nullable disable
+using SharpCompress.Common;
+
namespace SharpCompress.Compressors.PPMd.I1;
/// Allocate a single, large array and then provide sections of this array to callers. Callers are provided with
@@ -128,6 +130,9 @@ internal class Allocator
///
public void Start(int allocatorSize)
{
+ // PpmdProperties starts this allocator while parsing ZIP properties, so
+ // enforce the limit here, before any attacker-selected allocation.
+ DictionarySizeLimits.Validate(allocatorSize, DictionarySizeLimits.Ppmd, "PPMd");
var size = (uint)allocatorSize;
if (_allocatorSize != size)
{
diff --git a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.cs b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.cs
index 0d4c2175..a7bd2887 100644
--- a/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.cs
+++ b/src/SharpCompress/Compressors/Rar/UnpackV1/Unpack.cs
@@ -130,6 +130,7 @@ internal sealed partial class Unpack : BitInput, IRarUnpack
private void Init()
{
+ DictionarySizeLimits.Validate(PackDef.MAXWINSIZE, DictionarySizeLimits.Rar4, "RAR4");
if (this.window is null)
{
this.window = ArrayPool.Shared.Rent(PackDef.MAXWINSIZE);
diff --git a/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack_cpp.cs b/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack_cpp.cs
index 72c808f9..c094093f 100644
--- a/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack_cpp.cs
+++ b/src/SharpCompress/Compressors/Rar/UnpackV2017/Unpack.unpack_cpp.cs
@@ -99,14 +99,22 @@ internal sealed partial class Unpack : BitInput
WinSize = MinAllocSize;
}
+ // Validate before renting or reusing a solid window. The RAR5 header can
+ // request a huge dictionary even when there are no compressed bytes.
+ DictionarySizeLimits.Validate(
+ WinSize,
+ fileHeader.IsRar5 ? DictionarySizeLimits.Rar5 : DictionarySizeLimits.Rar4,
+ fileHeader.IsRar5 ? "RAR5" : "RAR4"
+ );
+
if (WinSize <= MaxWinSize) // Use the already allocated window.
{
return;
}
- if ((WinSize >> 16) > 0x10000) // Window size must not exceed 4 GB.
+ if (WinSize > int.MaxValue)
{
- return;
+ throw new InvalidFormatException("RAR window exceeds the supported array size.");
}
// Archiving code guarantees that window size does not grow in the same
diff --git a/src/SharpCompress/Compressors/ZStandard/Decompressor.cs b/src/SharpCompress/Compressors/ZStandard/Decompressor.cs
index 06da3b8f..4a0b0e8f 100644
--- a/src/SharpCompress/Compressors/ZStandard/Decompressor.cs
+++ b/src/SharpCompress/Compressors/ZStandard/Decompressor.cs
@@ -1,4 +1,5 @@
using System;
+using SharpCompress.Common;
using SharpCompress.Compressors.ZStandard.Unsafe;
namespace SharpCompress.Compressors.ZStandard;
@@ -10,6 +11,21 @@ public unsafe class Decompressor : IDisposable
public Decompressor()
{
handle = SafeDctxHandle.Create();
+ try
+ {
+ using var dctx = handle.Acquire();
+ Unsafe
+ .Methods.ZSTD_DCtx_setMaxWindowSize(
+ dctx,
+ checked((nuint)DictionarySizeLimits.ZStandard)
+ )
+ .EnsureZstdSuccess();
+ }
+ catch
+ {
+ handle.Dispose();
+ throw;
+ }
}
public void SetParameter(ZSTD_dParameter parameter, int value)
diff --git a/src/SharpCompress/Compressors/ZStandard/Unsafe/ZstdDecompress.cs b/src/SharpCompress/Compressors/ZStandard/Unsafe/ZstdDecompress.cs
index ab954fb6..d1b4be8f 100644
--- a/src/SharpCompress/Compressors/ZStandard/Unsafe/ZstdDecompress.cs
+++ b/src/SharpCompress/Compressors/ZStandard/Unsafe/ZstdDecompress.cs
@@ -3201,6 +3201,17 @@ public static unsafe partial class Methods
}
}
+ // Apply the limit before the single-pass shortcut too, so it
+ // does not depend on the caller's output buffer size.
+ zds->fParams.windowSize =
+ zds->fParams.windowSize > 1U << 10 ? zds->fParams.windowSize : 1U << 10;
+ if (zds->fParams.windowSize > zds->maxWindowSize)
+ {
+ return unchecked(
+ (nuint)(-(int)ZSTD_ErrorCode.ZSTD_error_frameParameter_windowTooLarge)
+ );
+ }
+
if (
zds->fParams.frameContentSize != unchecked(0UL - 1)
&& zds->fParams.frameType != ZSTD_frameType_e.ZSTD_skippableFrame
@@ -3282,15 +3293,6 @@ public static unsafe partial class Methods
zds->stage = ZSTD_dStage.ZSTDds_decodeBlockHeader;
}
- zds->fParams.windowSize =
- zds->fParams.windowSize > 1U << 10 ? zds->fParams.windowSize : 1U << 10;
- if (zds->fParams.windowSize > zds->maxWindowSize)
- {
- return unchecked(
- (nuint)(-(int)ZSTD_ErrorCode.ZSTD_error_frameParameter_windowTooLarge)
- );
- }
-
if (zds->maxBlockSizeParam != 0)
{
zds->fParams.blockSizeMax =
diff --git a/tests/SharpCompress.Test/Rar/RarDictionarySizeLimitTests.cs b/tests/SharpCompress.Test/Rar/RarDictionarySizeLimitTests.cs
new file mode 100644
index 00000000..6c85b8db
--- /dev/null
+++ b/tests/SharpCompress.Test/Rar/RarDictionarySizeLimitTests.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using SharpCompress.Archives;
+using SharpCompress.Common;
+using SharpCompress.Compressors.Xz;
+using SharpCompress.Readers;
+using SharpCompress.Test.Streams;
+using Xunit;
+
+namespace SharpCompress.Test.Rar;
+
+[Collection(DictionarySizeLimitCollection.Name)]
+public class RarDictionarySizeLimitTests
+{
+ // The advisory's complete 72-byte archives have valid header CRCs, no packed
+ // data, and just 45,056 bytes of declared output. Neither needs a large window.
+ private const string Rar256MiB =
+ "526172211a070100b28180510c01050800070101c496868000c63c6d612a02030b8080000480e00220c809b1cf8059000c6578652f746573742e6578650a03020058a2ce32ffc101";
+ private const string Rar1GiB =
+ "526172211a070100b28180510c01050800070101c4968680000fd7ea442a02030b8080000480e00220c809b1cf8069000c6578652f746573742e6578650a03020058a2ce32ffc101";
+
+ public static IEnumerable