Merge pull request #1024 from adamhathcock/copilot/fix-memory-exhaustion-bug

Fix memory exhaustion in TAR header auto-detection
This commit is contained in:
Adam Hathcock
2025-11-19 15:33:52 +00:00
committed by GitHub
3 changed files with 69 additions and 1 deletions

3
.gitignore vendored
View File

@@ -11,6 +11,8 @@ TestResults/
packages/*/
project.lock.json
tests/TestArchives/Scratch
tests/TestArchives/*/Scratch
tests/TestArchives/*/Scratch2
.vs
tools
.vscode
@@ -18,4 +20,3 @@ tools
.DS_Store
*.snupkg
/tests/TestArchives/6d23a38c-f064-4ef1-ad89-b942396f53b9/Scratch

View File

@@ -25,6 +25,10 @@ internal sealed class TarHeader
internal const int BLOCK_SIZE = 512;
// Maximum size for long name/link headers to prevent memory exhaustion attacks
// This is generous enough for most real-world scenarios (32KB)
private const int MAX_LONG_NAME_SIZE = 32768;
internal void Write(Stream output)
{
var buffer = new byte[BLOCK_SIZE];
@@ -186,6 +190,15 @@ internal sealed class TarHeader
private string ReadLongName(BinaryReader reader, byte[] buffer)
{
var size = ReadSize(buffer);
// Validate size to prevent memory exhaustion from malformed headers
if (size < 0 || size > MAX_LONG_NAME_SIZE)
{
throw new InvalidFormatException(
$"Long name size {size} is invalid or exceeds maximum allowed size of {MAX_LONG_NAME_SIZE} bytes"
);
}
var nameLength = (int)size;
var nameBytes = reader.ReadBytes(nameLength);
var remainingBytesToRead = BLOCK_SIZE - (nameLength % BLOCK_SIZE);

View File

@@ -254,4 +254,58 @@ public class TarReaderTests : ReaderTests
}
}
#endif
[Fact]
public void Tar_Malformed_LongName_Excessive_Size()
{
// Create a malformed TAR header with an excessively large LongName size
// This simulates what happens during auto-detection of compressed files
var buffer = new byte[512];
// Set up a basic TAR header structure
// Name field (offset 0, 100 bytes) - set to "././@LongLink" which is typical for LongName
var nameBytes = System.Text.Encoding.ASCII.GetBytes("././@LongLink");
Array.Copy(nameBytes, 0, buffer, 0, nameBytes.Length);
// Set entry type to LongName (offset 156)
buffer[156] = (byte)'L'; // EntryType.LongName
// Set an excessively large size (offset 124, 12 bytes, octal format)
// This simulates a corrupted/misinterpreted size field
// Using "77777777777" (octal) = 8589934591 bytes (~8GB)
var sizeBytes = System.Text.Encoding.ASCII.GetBytes("77777777777 ");
Array.Copy(sizeBytes, 0, buffer, 124, sizeBytes.Length);
// Calculate and set checksum (offset 148, 8 bytes)
// Set checksum field to spaces first
for (var i = 148; i < 156; i++)
{
buffer[i] = (byte)' ';
}
// Calculate checksum
var checksum = 0;
foreach (var b in buffer)
{
checksum += b;
}
var checksumStr = Convert.ToString(checksum, 8).PadLeft(6, '0') + "\0 ";
var checksumBytes = System.Text.Encoding.ASCII.GetBytes(checksumStr);
Array.Copy(checksumBytes, 0, buffer, 148, checksumBytes.Length);
// Create a stream with this malformed header
using var stream = new MemoryStream();
stream.Write(buffer, 0, buffer.Length);
stream.Position = 0;
// Attempt to read this malformed archive
// The InvalidFormatException from the validation gets caught and converted to IncompleteArchiveException
// The important thing is it doesn't cause OutOfMemoryException
Assert.Throws<IncompleteArchiveException>(() =>
{
using var reader = TarReader.Open(stream);
reader.MoveToNextEntry();
});
}
}