Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,13 @@ internal void ReplaceNormalAttributesWithExtended(IEnumerable<KeyValuePair<strin
}

// The 'uid' header field only fits 8 bytes, or the user could've stored an override in the extended attributes
if (TarHelpers.TryGetStringAsBaseTenInteger(values.Uid, out int uid))
if (TarHelpers.TryGetStringAsUidGid(values.Uid, out int uid))
{
_uid = uid;
}

// The 'gid' header field only fits 8 bytes, or the user could've stored an override in the extended attributes
if (TarHelpers.TryGetStringAsBaseTenInteger(values.Gid, out int gid))
if (TarHelpers.TryGetStringAsUidGid(values.Gid, out int gid))
{
_gid = gid;
}
Expand Down Expand Up @@ -453,8 +453,8 @@ internal async ValueTask ProcessDataBlockCoreAsync<TAdapter>(Stream archiveStrea
{
_checksum = checksum,
_size = size,
_uid = TarHelpers.ParseNumeric<int>(buffer.Slice(FieldLocations.Uid, FieldLengths.Uid)),
_gid = TarHelpers.ParseNumeric<int>(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))
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,15 +621,8 @@ private int WriteCommonFields(Span<byte> 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)
{
Expand Down Expand Up @@ -1056,6 +1049,26 @@ private int FormatNumeric(int value, Span<byte> 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<byte> 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<byte> destination)
{
Debug.Assert(destination.Length == 12, "12 byte field expected.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -266,8 +269,8 @@ private void AddOrUpdateStandardFieldExtendedAttributes(Dictionary<string, strin
}

AddOrRemoveNumericField(ea, PaxEaSize, _size, Octal12ByteFieldMaxValue, removeIfUnneeded);
AddOrRemoveNumericField(ea, PaxEaUid, _uid, Octal8ByteFieldMaxValue, removeIfUnneeded);
AddOrRemoveNumericField(ea, PaxEaGid, _gid, Octal8ByteFieldMaxValue, removeIfUnneeded);
AddOrRemoveNumericField(ea, PaxEaUid, UidGidAsUnsigned(_uid), Octal8ByteFieldMaxValue, removeIfUnneeded);
AddOrRemoveNumericField(ea, PaxEaGid, UidGidAsUnsigned(_gid), Octal8ByteFieldMaxValue, removeIfUnneeded);
AddOrRemoveNumericField(ea, PaxEaDevMajor, _devMajor, Octal8ByteFieldMaxValue, removeIfUnneeded);
AddOrRemoveNumericField(ea, PaxEaDevMinor, _devMinor, Octal8ByteFieldMaxValue, removeIfUnneeded);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,19 @@ internal static bool TryGetStringAsBaseTenInteger(IReadOnlyDictionary<string, st
out baseTenInteger);
}

/// <summary>Parses a uid or gid extended attribute value. See <see cref="ParseUidGid"/> for how out of range values are handled.</summary>
internal static bool TryGetStringAsUidGid(string? value, out int id)
{
if (!string.IsNullOrEmpty(value))
{
id = ToUidGid(long.Parse(value, CultureInfo.InvariantCulture));
return true;
}

id = 0;
return false;
}

internal static bool TryGetStringAsBaseTenInteger(string? value, out int baseTenInteger)
{
if (!string.IsNullOrEmpty(value))
Expand Down Expand Up @@ -208,6 +221,25 @@ internal static TarEntryType GetCorrectTypeFlagForFormat(TarEntryFormat format,
_ => entryType,
};

/// <summary>Parses a uid or gid numeric field.</summary>
/// <remarks>
/// Unix uid_t and gid_t are 32-bit unsigned, and archives may contain values larger than <see cref="int.MaxValue"/>
/// (for example, GNU base-256 encoded fields). Such values are reinterpreted as <see cref="int"/> without an
/// overflow check, which matches how <see cref="TarWriter"/> 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.
/// </remarks>
internal static int ParseUidGid(ReadOnlySpan<byte> buffer) => ToUidGid(ParseNumeric<long>(buffer));

private static int ToUidGid(long value)
{
if (value < int.MinValue || value > uint.MaxValue)
{
ThrowInvalidNumber();
}

return unchecked((int)value);
}

/// <summary>Parses a numeric field.</summary>
internal static T ParseNumeric<T>(ReadOnlySpan<byte> buffer) where T : struct, INumber<T>, IBinaryInteger<T>
{
Expand Down
119 changes: 119 additions & 0 deletions src/libraries/System.Formats.Tar/tests/TarReader/TarReader.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,5 +179,124 @@ public void TarReader_InvalidChecksum_ThrowsException(bool corrupted)
Assert.Contains("Checksum", exception.Message);
}
}

public static IEnumerable<object[]> 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(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<InvalidDataException>(() => 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<string, string>() { { "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<InvalidDataException>(() => reader.GetNextEntry());
}

[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<string, string>() { { "uid", value }, { "gid", value } }));
}

stream.Position = 0;
using TarReader reader = new TarReader(stream);
PaxTarEntry entry = Assert.IsType<PaxTarEntry>(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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -288,5 +288,39 @@ public async Task PaxExtendedAttributes_DoNotOverwritePublicProperties_WhenLarge
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"]);
}
}
}
}
Loading