From 63a57e7648975068935f441d75e01c65bbcf10af Mon Sep 17 00:00:00 2001 From: Adeel Mujahid <3840695+am11@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:32:46 +0000 Subject: [PATCH 1/3] Handle TAR uid/gid values larger than int.MaxValue --- .../src/System/Formats/Tar/TarEntry.cs | 4 +- .../src/System/Formats/Tar/TarHeader.Read.cs | 8 +- .../src/System/Formats/Tar/TarHeader.Write.cs | 31 +++++-- .../src/System/Formats/Tar/TarHeader.cs | 9 +- .../src/System/Formats/Tar/TarHelpers.cs | 21 +++++ .../tests/TarReader/TarReader.Tests.cs | 88 +++++++++++++++++++ ...Writer.WriteEntry.Entry.Roundtrip.Tests.cs | 34 +++++++ 7 files changed, 177 insertions(+), 18 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index 560790ce46a4b6..ac9fbd5a518a70 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -91,7 +91,7 @@ public int Gid set { _header._gid = value; - _header.SyncNumericExtendedAttribute(TarHeader.PaxEaGid, value, TarHeader.Octal8ByteFieldMaxValue); + _header.SyncNumericExtendedAttribute(TarHeader.PaxEaGid, TarHeader.UidGidAsUnsigned(value), TarHeader.Octal8ByteFieldMaxValue); } } @@ -186,7 +186,7 @@ public int Uid set { _header._uid = value; - _header.SyncNumericExtendedAttribute(TarHeader.PaxEaUid, value, TarHeader.Octal8ByteFieldMaxValue); + _header.SyncNumericExtendedAttribute(TarHeader.PaxEaUid, TarHeader.UidGidAsUnsigned(value), TarHeader.Octal8ByteFieldMaxValue); } } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs index 17f10dae7a0989..071e39712023a4 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs @@ -231,13 +231,13 @@ internal void ReplaceNormalAttributesWithExtended(IEnumerable(Stream archiveStrea { _checksum = checksum, _size = size, - _uid = TarHelpers.ParseNumeric(buffer.Slice(FieldLocations.Uid, FieldLengths.Uid)), - _gid = TarHelpers.ParseNumeric(buffer.Slice(FieldLocations.Gid, FieldLengths.Gid)), + _uid = TarHelpers.ParseUidGid(buffer.Slice(FieldLocations.Uid, FieldLengths.Uid)), + _gid = TarHelpers.ParseUidGid(buffer.Slice(FieldLocations.Gid, FieldLengths.Gid)), _linkName = TarHelpers.ParseUtf8String(buffer.Slice(FieldLocations.LinkName, FieldLengths.LinkName)) }; diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs index 16df825fa04515..28f13090b1c45f 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Write.cs @@ -620,15 +620,8 @@ private int WriteCommonFields(Span buffer, TarEntryType actualEntryType) checksum += FormatNumeric(_mode, buffer.Slice(FieldLocations.Mode, FieldLengths.Mode)); } - if (_uid >= 0) - { - checksum += FormatNumeric(_uid, buffer.Slice(FieldLocations.Uid, FieldLengths.Uid)); - } - - if (_gid >= 0) - { - checksum += FormatNumeric(_gid, buffer.Slice(FieldLocations.Gid, FieldLengths.Gid)); - } + checksum += FormatUidGid(_uid, buffer.Slice(FieldLocations.Uid, FieldLengths.Uid)); + checksum += FormatUidGid(_gid, buffer.Slice(FieldLocations.Gid, FieldLengths.Gid)); if (_size >= 0) { @@ -1055,6 +1048,26 @@ private int FormatNumeric(int value, Span destination) } } + // Unix uid_t and gid_t are unsigned: a negative value represents an id larger than int.MaxValue. + // Only the GNU format can store those ids in the header field. PAX stores them in the extended attributes, + // and for the other formats the field is left empty. + private int FormatUidGid(int value, Span destination) + { + if (value >= 0) + { + return FormatNumeric(value, destination); + } + + if (_format == TarEntryFormat.Gnu) + { + // Store in big endian format with leading '0x80' byte. + BinaryPrimitives.WriteInt64BigEndian(destination, (long)unchecked((uint)value) | (1L << 63)); + return Checksum(destination); + } + + return 0; + } + private int FormatNumeric(long value, Span destination) { Debug.Assert(destination.Length == 12, "12 byte field expected."); diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.cs index 324777ff3aeab6..d0579f9ec10d7e 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.cs @@ -216,7 +216,10 @@ internal void SyncTimestampExtendedAttribute(string key, DateTimeOffset value) // Only updates if the format is PAX and the ExtendedAttributes dictionary has been initialized. // Uses the same logic as CollectExtendedAttributesFromStandardFieldsIfNeeded to determine // whether to add or remove the attribute. - internal void SyncNumericExtendedAttribute(string key, int value, int maxNonextendedValue) + // Unix uid_t and gid_t are unsigned: a negative value represents an id larger than int.MaxValue. + internal static long UidGidAsUnsigned(int value) => unchecked((uint)value); + + internal void SyncNumericExtendedAttribute(string key, long value, int maxNonextendedValue) { if (_format == TarEntryFormat.Pax && _ea is not null) { @@ -266,8 +269,8 @@ private void AddOrUpdateStandardFieldExtendedAttributes(DictionaryParses a uid or gid extended attribute value. See for how out of range values are handled. + internal static bool TryGetStringAsUidGid(string? value, out int id) + { + if (!string.IsNullOrEmpty(value)) + { + id = unchecked((int)long.Parse(value, CultureInfo.InvariantCulture)); + return true; + } + + id = 0; + return false; + } + internal static bool TryGetStringAsBaseTenInteger(string? value, out int baseTenInteger) { if (!string.IsNullOrEmpty(value)) @@ -208,6 +221,14 @@ internal static TarEntryType GetCorrectTypeFlagForFormat(TarEntryFormat format, _ => entryType, }; + /// Parses a uid or gid numeric field. + /// + /// Unix uid_t and gid_t are unsigned, and archives may contain values larger than + /// (for example, GNU base-256 encoded fields). The value is reinterpreted as without an + /// overflow check, which matches how stores uid and gid values read from the file system. + /// + internal static int ParseUidGid(ReadOnlySpan buffer) => unchecked((int)ParseNumeric(buffer)); + /// Parses a numeric field. internal static T ParseNumeric(ReadOnlySpan buffer) where T : struct, INumber, IBinaryInteger { diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.Tests.cs index 8492be776ae965..b14a1e3485667c 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.Tests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using Xunit; namespace System.Formats.Tar.Tests @@ -166,5 +167,92 @@ public void TarReader_InvalidChecksum_ThrowsException(bool corrupted) Assert.Contains("Checksum", exception.Message); } } + + public static IEnumerable GnuBase256UidGidTestData() + { + // Leading 0x80 byte: the remaining bytes are a positive big-endian value. + yield return new object[] { new byte[] { 0x80, 0, 0, 0, 0x7F, 0xFF, 0xFF, 0xFF }, int.MaxValue }; + yield return new object[] { new byte[] { 0x80, 0, 0, 0, 0xB6, 0x5A, 0x65, 0x38 }, unchecked((int)0xB65A6538u) }; + yield return new object[] { new byte[] { 0x80, 0, 0, 0, 0xFF, 0x5A, 0x65, 0x38 }, unchecked((int)0xFF5A6538u) }; + yield return new object[] { new byte[] { 0x80, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF }, -1 }; + // Leading 0xFF byte: the field is a negative big-endian value. + yield return new object[] { new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE }, -2 }; + } + + [Theory] + [MemberData(nameof(GnuBase256UidGidTestData))] + public void GnuBase256UidGid_LargerThanInt32MaxValue_DoesNotThrow(byte[] fieldBytes, int expected) + { + byte[] tarData = CreateEntryWithRawUidGid(fieldBytes, fieldBytes); + + using TarReader reader = new TarReader(new MemoryStream(tarData)); + TarEntry entry = reader.GetNextEntry(); + Assert.NotNull(entry); + Assert.Equal(expected, entry.Uid); + Assert.Equal(expected, entry.Gid); + Assert.Null(reader.GetNextEntry()); + } + + [Theory] + [MemberData(nameof(GnuBase256UidGidTestData))] + public async Task GnuBase256UidGid_LargerThanInt32MaxValue_DoesNotThrow_Async(byte[] fieldBytes, int expected) + { + byte[] tarData = CreateEntryWithRawUidGid(fieldBytes, fieldBytes); + + await using TarReader reader = new TarReader(new MemoryStream(tarData)); + TarEntry entry = await reader.GetNextEntryAsync(); + Assert.NotNull(entry); + Assert.Equal(expected, entry.Uid); + Assert.Equal(expected, entry.Gid); + Assert.Null(await reader.GetNextEntryAsync()); + } + + [Theory] + [InlineData("2147483647", int.MaxValue)] + [InlineData("3059377464", unchecked((int)3059377464u))] + [InlineData("4294967295", -1)] + public void PaxUidGid_LargerThanInt32MaxValue_DoesNotThrow(string value, int expected) + { + MemoryStream stream = new MemoryStream(); + using (TarWriter writer = new TarWriter(stream, leaveOpen: true)) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "dir", new Dictionary() { { "uid", value }, { "gid", value } })); + } + + stream.Position = 0; + using TarReader reader = new TarReader(stream); + PaxTarEntry entry = Assert.IsType(reader.GetNextEntry()); + Assert.Equal(expected, entry.Uid); + Assert.Equal(expected, entry.Gid); + Assert.Equal(value, entry.ExtendedAttributes["uid"]); + Assert.Equal(value, entry.ExtendedAttributes["gid"]); + } + + // Writes a GNU entry, then overwrites its uid and gid fields with the given raw bytes and fixes up the checksum. + private static byte[] CreateEntryWithRawUidGid(byte[] uid, byte[] gid) + { + const int UidOffset = 108, GidOffset = 116, ChecksumOffset = 148, FieldLength = 8; + + MemoryStream ms = new MemoryStream(); + using (TarWriter writer = new TarWriter(ms, TarEntryFormat.Gnu, leaveOpen: true)) + { + writer.WriteEntry(new GnuTarEntry(TarEntryType.Directory, "dir")); + } + + byte[] tarData = ms.ToArray(); + uid.CopyTo(tarData, UidOffset); + gid.CopyTo(tarData, GidOffset); + + // The checksum is computed with the checksum field itself filled with spaces. + tarData.AsSpan(ChecksumOffset, FieldLength).Fill((byte)' '); + int checksum = 0; + for (int i = 0; i < 512; i++) + { + checksum += tarData[i]; + } + System.Text.Encoding.ASCII.GetBytes(Convert.ToString(checksum, 8).PadLeft(6, '0') + "\0 ").CopyTo(tarData, ChecksumOffset); + + return tarData; + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Roundtrip.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Roundtrip.Tests.cs index 51c64eec7a0cdf..3c1f452c7615e0 100644 --- a/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Roundtrip.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarWriter/TarWriter.WriteEntry.Entry.Roundtrip.Tests.cs @@ -247,5 +247,39 @@ public void PaxExtendedAttributes_DoNotOverwritePublicProperties_WhenLargerThanL Assert.Equal(writeEntry.ModificationTime, readEntry.ModificationTime); Assert.Equal(writeEntry.LinkName, readEntry.LinkName); } + + [Theory] + [InlineData(TarEntryFormat.Gnu, int.MaxValue)] + [InlineData(TarEntryFormat.Gnu, unchecked((int)0xB65A6538u))] + [InlineData(TarEntryFormat.Gnu, -1)] + [InlineData(TarEntryFormat.Pax, int.MaxValue)] + [InlineData(TarEntryFormat.Pax, unchecked((int)0xB65A6538u))] + [InlineData(TarEntryFormat.Pax, -1)] + public void UidGid_LargerThanInt32MaxValue_Roundtrips(TarEntryFormat format, int id) + { + TarEntry writeEntry = InvokeTarEntryCreationConstructor(format, TarEntryType.Directory, "dir"); + writeEntry.Uid = id; + writeEntry.Gid = id; + + using MemoryStream ms = new MemoryStream(); + using (TarWriter writer = new TarWriter(ms, leaveOpen: true)) + { + writer.WriteEntry(writeEntry); + } + + ms.Position = 0; + using TarReader reader = new TarReader(ms); + TarEntry readEntry = reader.GetNextEntry(); + Assert.NotNull(readEntry); + Assert.Equal(id, readEntry.Uid); + Assert.Equal(id, readEntry.Gid); + + if (readEntry is PaxTarEntry paxEntry && id < 0) + { + string expected = unchecked((uint)id).ToString(); + Assert.Equal(expected, paxEntry.ExtendedAttributes["uid"]); + Assert.Equal(expected, paxEntry.ExtendedAttributes["gid"]); + } + } } } From 6f230810bb2f28cbe53b01f98299bcbf224ae5d7 Mon Sep 17 00:00:00 2001 From: Adeel Mujahid <3840695+am11@users.noreply.github.com> Date: Tue, 29 Sep 2026 19:37:32 +0300 Subject: [PATCH 2/3] Address CR feedback --- .../src/System/Formats/Tar/TarHelpers.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs index a0debece28d824..128b06b38c99ef 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs @@ -167,7 +167,7 @@ internal static bool TryGetStringAsUidGid(string? value, out int id) { if (!string.IsNullOrEmpty(value)) { - id = unchecked((int)long.Parse(value, CultureInfo.InvariantCulture)); + id = ToUidGid(long.Parse(value, CultureInfo.InvariantCulture)); return true; } @@ -223,11 +223,22 @@ internal static TarEntryType GetCorrectTypeFlagForFormat(TarEntryFormat format, /// Parses a uid or gid numeric field. /// - /// Unix uid_t and gid_t are unsigned, and archives may contain values larger than - /// (for example, GNU base-256 encoded fields). The value is reinterpreted as without an + /// Unix uid_t and gid_t are 32-bit unsigned, and archives may contain values larger than + /// (for example, GNU base-256 encoded fields). Such values are reinterpreted as without an /// overflow check, which matches how stores uid and gid values read from the file system. + /// Values that don't fit in 32 bits can't be a valid id and are rejected. /// - internal static int ParseUidGid(ReadOnlySpan buffer) => unchecked((int)ParseNumeric(buffer)); + internal static int ParseUidGid(ReadOnlySpan buffer) => ToUidGid(ParseNumeric(buffer)); + + private static int ToUidGid(long value) + { + if (value < int.MinValue || value > uint.MaxValue) + { + ThrowInvalidNumber(); + } + + return unchecked((int)value); + } /// Parses a numeric field. internal static T ParseNumeric(ReadOnlySpan buffer) where T : struct, INumber, IBinaryInteger From b1d4b4561aee94f74e3d6a23d819a259449f67c3 Mon Sep 17 00:00:00 2001 From: Adeel Mujahid <3840695+am11@users.noreply.github.com> Date: Tue, 29 Sep 2026 19:38:09 +0300 Subject: [PATCH 3/3] More tests --- .../tests/TarReader/TarReader.Tests.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.Tests.cs index b14a1e3485667c..22746ba5ca47ae 100644 --- a/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarReader/TarReader.Tests.cs @@ -207,6 +207,38 @@ public async Task GnuBase256UidGid_LargerThanInt32MaxValue_DoesNotThrow_Async(by Assert.Null(await reader.GetNextEntryAsync()); } + [Theory] + [InlineData(new byte[] { 0x80, 0, 0, 1, 0, 0, 0, 0 })] // uint.MaxValue + 1 + [InlineData(new byte[] { 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF })] + [InlineData(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF })] // int.MinValue - 1 + public void GnuBase256UidGid_LargerThan32Bits_Throws(byte[] fieldBytes) + { + byte[] tarData = CreateEntryWithRawUidGid(fieldBytes, fieldBytes); + + using TarReader reader = new TarReader(new MemoryStream(tarData)); + Assert.Throws(() => reader.GetNextEntry()); + } + + [Fact] + public void PaxUidGid_LargerThan32Bits_Throws() + { + // Write a valid 10 digit uid, then replace it in the extended attributes with one larger than uint.MaxValue. + MemoryStream stream = new MemoryStream(); + using (TarWriter writer = new TarWriter(stream, leaveOpen: true)) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "dir", new Dictionary() { { "uid", "3059377464" } })); + } + + byte[] tarData = stream.ToArray(); + byte[] original = System.Text.Encoding.ASCII.GetBytes("uid=3059377464"); + int index = tarData.AsSpan().IndexOf(original); + Assert.True(index >= 0); + System.Text.Encoding.ASCII.GetBytes("uid=9999999999").CopyTo(tarData, index); + + using TarReader reader = new TarReader(new MemoryStream(tarData)); + Assert.Throws(() => reader.GetNextEntry()); + } + [Theory] [InlineData("2147483647", int.MaxValue)] [InlineData("3059377464", unchecked((int)3059377464u))]