Skip to content

Commit dbcfed4

Browse files
[typemap] Harden manifest and DEX validation
Resolve manifest packages and placeholders consistently before deduplication, and validate DEX and modified UTF-8 inputs without lossy or unbounded decoding. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 3bcda3d commit dbcfed4

8 files changed

Lines changed: 390 additions & 40 deletions

File tree

external/Java.Interop/src/Xamarin.Android.Tools.Bytecode/ConstantPool.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,18 +390,26 @@ public ConstantPoolUtf8Item (ConstantPool constantPool, Stream stream)
390390
for (int i = 0; i < data.Length; ++i) {
391391
byte first = data [i];
392392
if ((first & 0x80) == 0) {
393+
if (first == 0)
394+
throw new InvalidDataException ("Modified UTF-8 contains a raw null byte.");
393395
decoded.Append ((char) first);
394396
continue;
395397
}
396398
if ((first & 0xe0) == 0xc0 && i + 1 < data.Length) {
397399
byte second = ReadContinuationByte (data [++i]);
398-
decoded.Append ((char) (((first & 0x1f) << 6) | (second & 0x3f)));
400+
char value = (char) (((first & 0x1f) << 6) | (second & 0x3f));
401+
if (value < '\u0080' && (first != 0xc0 || second != 0x80))
402+
throw new InvalidDataException ("Modified UTF-8 contains an invalid two-byte overlong encoding.");
403+
decoded.Append (value);
399404
continue;
400405
}
401406
if ((first & 0xf0) == 0xe0 && i + 2 < data.Length) {
402407
byte second = ReadContinuationByte (data [++i]);
403408
byte third = ReadContinuationByte (data [++i]);
404-
decoded.Append ((char) (((first & 0x0f) << 12) | ((second & 0x3f) << 6) | (third & 0x3f)));
409+
char value = (char) (((first & 0x0f) << 12) | ((second & 0x3f) << 6) | (third & 0x3f));
410+
if (value < '\u0800')
411+
throw new InvalidDataException ("Modified UTF-8 contains an invalid three-byte overlong encoding.");
412+
decoded.Append (value);
405413
continue;
406414
}
407415
throw new InvalidDataException ($"Invalid modified UTF-8 lead byte 0x{first:x2}.");

external/Java.Interop/tests/Xamarin.Android.Tools.Bytecode-Tests/ClassFileTests.cs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public void ModifiedUtf8_DecodesUtf16CodeUnits ()
2323
using var poolStream = new MemoryStream (new byte [] { 0, 1 });
2424
var pool = new ConstantPool (poolStream);
2525

26+
Assert.AreEqual ("\0", Decode (pool, new byte [] { 0xc0, 0x80 }));
27+
Assert.AreEqual ("\u0080\u0800", Decode (pool, new byte [] { 0xc2, 0x80, 0xe0, 0xa0, 0x80 }));
2628
Assert.AreEqual (
2729
"\ud000\ud001",
2830
Decode (pool, new byte [] { 0xed, 0x80, 0x80, 0xed, 0x80, 0x81 })
@@ -31,16 +33,35 @@ public void ModifiedUtf8_DecodesUtf16CodeUnits ()
3133
"\U00010400",
3234
Decode (pool, new byte [] { 0xed, 0xa0, 0x81, 0xed, 0xb0, 0x80 })
3335
);
36+
Assert.AreEqual (
37+
"\ud801\udc00\ud801",
38+
Decode (pool, new byte [] { 0xed, 0xa0, 0x81, 0xed, 0xb0, 0x80, 0xed, 0xa0, 0x81 })
39+
);
40+
}
3441

35-
static string Decode (ConstantPool pool, byte [] bytes)
36-
{
37-
using var stream = new MemoryStream ();
38-
stream.WriteByte ((byte) (bytes.Length >> 8));
39-
stream.WriteByte ((byte) bytes.Length);
40-
stream.Write (bytes, 0, bytes.Length);
41-
stream.Position = 0;
42-
return new ConstantPoolUtf8Item (pool, stream).Value;
42+
[Test]
43+
public void ModifiedUtf8_RejectsMalformedEncodings ()
44+
{
45+
using var poolStream = new MemoryStream (new byte [] { 0, 1 });
46+
var pool = new ConstantPool (poolStream);
47+
foreach (var bytes in new [] {
48+
new byte [] { 0 },
49+
new byte [] { 0xc0, 0x81 },
50+
new byte [] { 0xc1, 0xbf },
51+
new byte [] { 0xe0, 0x81, 0x81 },
52+
}) {
53+
Assert.Throws<InvalidDataException> (() => Decode (pool, bytes));
4354
}
4455
}
56+
57+
static string Decode (ConstantPool pool, byte [] bytes)
58+
{
59+
using var stream = new MemoryStream ();
60+
stream.WriteByte ((byte) (bytes.Length >> 8));
61+
stream.WriteByte ((byte) bytes.Length);
62+
stream.Write (bytes, 0, bytes.Length);
63+
stream.Position = 0;
64+
return new ConstantPoolUtf8Item (pool, stream).Value;
65+
}
4566
}
4667
}

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ void MergeLibraryManifests (XElement manifest)
201201
continue;
202202
}
203203

204+
ApplyPlaceholders (libDoc, ManifestPlaceholders, PackageName);
204205
var package = (string?) libRoot.Attribute ("package") ?? "";
205206
foreach (var top in libRoot.Elements ().ToList ()) {
206207
var name = (string?) top.Attribute (AndroidNs + "name");
@@ -343,10 +344,7 @@ void EnsureManifestAttributes (XElement manifest)
343344
// produced by GetAndroidPackageName, which substitutes placeholders and canonicalizes the
344345
// package). This matches the legacy GenerateMainAndroidManifest; a valid explicit package is
345346
// preserved so compat-name resolution keeps using it.
346-
var packageAttr = (string?) manifest.Attribute ("package") ?? "";
347-
if ((packageAttr.Length == 0 || packageAttr.Contains ("${")) && !PackageName.IsNullOrEmpty ()) {
348-
manifest.SetAttributeValue ("package", PackageName);
349-
}
347+
ResolvePackageName (manifest, PackageName);
350348

351349
if (manifest.Attribute (AndroidNs + "versionCode") is null) {
352350
manifest.SetAttributeValue (AndroidNs + "versionCode",
@@ -378,6 +376,14 @@ void EnsureManifestAttributes (XElement manifest)
378376
}
379377
}
380378

379+
internal static void ResolvePackageName (XElement manifest, string? packageName)
380+
{
381+
var packageAttr = (string?) manifest.Attribute ("package") ?? "";
382+
if ((packageAttr.Length == 0 || packageAttr.Contains ("${")) && !packageName.IsNullOrEmpty ()) {
383+
manifest.SetAttributeValue ("package", packageName);
384+
}
385+
}
386+
381387
XElement EnsureApplicationElement (XElement manifest)
382388
{
383389
var app = manifest.Element ("application");

src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -677,10 +677,7 @@ static void AddPeerByDotName (Dictionary<string, List<JavaPeerInfo>> peersByDotN
677677
return doc;
678678
}
679679

680-
if (((string?) root.Attribute ("package")).IsNullOrEmpty () && !manifestConfig.PackageName.IsNullOrEmpty ()) {
681-
root.SetAttributeValue ("package", manifestConfig.PackageName);
682-
}
683-
680+
ManifestGenerator.ResolvePackageName (root, manifestConfig.PackageName);
684681
ManifestGenerator.ApplyPlaceholders (doc, manifestConfig.ManifestPlaceholders, manifestConfig.PackageName);
685682

686683
if (!manifestConfig.ApplicationJavaClass.IsNullOrEmpty ()) {

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,160 @@ public void DexUtils_RejectsDex041Containers ()
884884
Assert.Throws<NotSupportedException> (() => DexUtils.GetClassDescriptors (dexFile));
885885
}
886886

887+
[TestCase ("magic-terminator")]
888+
[TestCase ("header-size")]
889+
[TestCase ("file-size")]
890+
[TestCase ("map-missing")]
891+
[TestCase ("map-section")]
892+
[TestCase ("string-section")]
893+
[TestCase ("data-section")]
894+
public void DexUtils_RejectsMalformedHeaders (string malformedField)
895+
{
896+
var dex = CreateMinimalDex ([(byte) 'L', (byte) 'x', (byte) ';'], utf16Length: 3);
897+
switch (malformedField) {
898+
case "magic-terminator":
899+
dex [7] = 1;
900+
break;
901+
case "header-size":
902+
WriteUInt32 (dex, 36, 111);
903+
break;
904+
case "file-size":
905+
WriteUInt32 (dex, 32, checked ((uint) dex.Length - 1));
906+
break;
907+
case "map-missing":
908+
WriteUInt32 (dex, 52, 0);
909+
break;
910+
case "map-section":
911+
WriteUInt32 (dex, checked ((int) ReadUInt32 (dex, 52)), uint.MaxValue);
912+
break;
913+
case "string-section":
914+
WriteUInt32 (dex, 56, uint.MaxValue);
915+
break;
916+
case "data-section":
917+
WriteUInt32 (dex, 104, uint.MaxValue);
918+
break;
919+
default:
920+
throw new InvalidOperationException ($"Unknown malformed DEX field '{malformedField}'.");
921+
}
922+
923+
var dexFile = WriteDexFile (dex, malformedField);
924+
Assert.Throws<InvalidDataException> (() => DexUtils.GetClassDescriptors (dexFile));
925+
}
926+
927+
[Test]
928+
public void DexUtils_DecodesModifiedUtf8 ()
929+
{
930+
var bmpDex = CreateMinimalDex (
931+
[(byte) 'L', 0xc2, 0x80, 0xe0, 0xa0, 0x80, (byte) ';'],
932+
utf16Length: 4);
933+
var nullDex = CreateMinimalDex (
934+
[(byte) 'L', 0xc0, 0x80, (byte) ';'],
935+
utf16Length: 3);
936+
var supplementaryDex = CreateMinimalDex (
937+
[(byte) 'L', (byte) 'x', (byte) '/', 0xed, 0xa0, 0x81, 0xed, 0xb0, 0x80, (byte) ';'],
938+
utf16Length: 6);
939+
940+
Assert.AreEqual ("L\u0080\u0800;", DexUtils.GetClassDescriptors (WriteDexFile (bmpDex, "bmp")).Single ());
941+
Assert.AreEqual ("L\0;", DexUtils.GetClassDescriptors (WriteDexFile (nullDex, "null")).Single ());
942+
Assert.AreEqual ("Lx/\U00010400;", DexUtils.GetClassDescriptors (WriteDexFile (supplementaryDex, "supplementary")).Single ());
943+
}
944+
945+
[Test]
946+
public void DexUtils_RejectsMalformedModifiedUtf8 ()
947+
{
948+
foreach (var (name, bytes, length) in new [] {
949+
("raw-null", new byte [] { (byte) 'L', 0, (byte) 'x', (byte) ';' }, 4u),
950+
("two-byte-overlong", new byte [] { (byte) 'L', 0xc0, 0x81, (byte) ';' }, 3u),
951+
("two-byte-overlong-c1", new byte [] { (byte) 'L', 0xc1, 0xbf, (byte) ';' }, 3u),
952+
("three-byte-overlong", new byte [] { (byte) 'L', 0xe0, 0x81, 0x81, (byte) ';' }, 3u),
953+
("unmatched-high", new byte [] { (byte) 'L', 0xed, 0xa0, 0x81, (byte) ';' }, 3u),
954+
("unmatched-low", new byte [] { (byte) 'L', 0xed, 0xb0, 0x80, (byte) ';' }, 3u),
955+
}) {
956+
var dexFile = WriteDexFile (CreateMinimalDex (bytes, length), name);
957+
Assert.Throws<InvalidDataException> (() => DexUtils.GetClassDescriptors (dexFile));
958+
}
959+
}
960+
961+
[Test]
962+
public void DexUtils_RejectsUnboundedDeclaredStringLength ()
963+
{
964+
var dex = CreateMinimalDex ([], uint.MaxValue);
965+
var dexFile = WriteDexFile (dex, "huge-string");
966+
967+
Assert.Throws<InvalidDataException> (() => DexUtils.GetClassDescriptors (dexFile));
968+
}
969+
970+
string WriteDexFile (byte [] dex, string name)
971+
{
972+
var directory = Path.Combine (Root, "temp", TestName);
973+
Directory.CreateDirectory (directory);
974+
var path = Path.Combine (directory, name + ".dex");
975+
File.WriteAllBytes (path, dex);
976+
return path;
977+
}
978+
979+
static byte [] CreateMinimalDex (byte [] modifiedUtf8, uint utf16Length)
980+
{
981+
var length = EncodeUnsignedLeb128 (utf16Length);
982+
const int stringIdsOffset = 112;
983+
const int typeIdsOffset = 116;
984+
const int classDefsOffset = 120;
985+
const int stringDataOffset = 152;
986+
int mapOffset = stringDataOffset + length.Length + modifiedUtf8.Length + 1;
987+
var dex = new byte [mapOffset + 16];
988+
Encoding.ASCII.GetBytes ("dex\n039\0").CopyTo (dex, 0);
989+
WriteUInt32 (dex, 32, checked ((uint) dex.Length));
990+
WriteUInt32 (dex, 36, 112);
991+
WriteUInt32 (dex, 40, 0x12345678);
992+
WriteUInt32 (dex, 52, checked ((uint) mapOffset));
993+
WriteUInt32 (dex, 56, 1);
994+
WriteUInt32 (dex, 60, stringIdsOffset);
995+
WriteUInt32 (dex, 64, 1);
996+
WriteUInt32 (dex, 68, typeIdsOffset);
997+
WriteUInt32 (dex, 96, 1);
998+
WriteUInt32 (dex, 100, classDefsOffset);
999+
WriteUInt32 (dex, 104, checked ((uint) (dex.Length - stringDataOffset)));
1000+
WriteUInt32 (dex, 108, stringDataOffset);
1001+
WriteUInt32 (dex, stringIdsOffset, stringDataOffset);
1002+
length.CopyTo (dex, stringDataOffset);
1003+
modifiedUtf8.CopyTo (dex, stringDataOffset + length.Length);
1004+
WriteUInt32 (dex, mapOffset, 1);
1005+
dex [mapOffset + 4] = 0x00;
1006+
dex [mapOffset + 5] = 0x10;
1007+
WriteUInt32 (dex, mapOffset + 8, 1);
1008+
WriteUInt32 (dex, mapOffset + 12, checked ((uint) mapOffset));
1009+
return dex;
1010+
}
1011+
1012+
static byte [] EncodeUnsignedLeb128 (uint value)
1013+
{
1014+
var bytes = new List<byte> ();
1015+
do {
1016+
byte next = (byte) (value & 0x7f);
1017+
value >>= 7;
1018+
if (value != 0) {
1019+
next |= 0x80;
1020+
}
1021+
bytes.Add (next);
1022+
} while (value != 0);
1023+
return bytes.ToArray ();
1024+
}
1025+
1026+
static void WriteUInt32 (byte [] data, int offset, uint value)
1027+
{
1028+
data [offset] = (byte) value;
1029+
data [offset + 1] = (byte) (value >> 8);
1030+
data [offset + 2] = (byte) (value >> 16);
1031+
data [offset + 3] = (byte) (value >> 24);
1032+
}
1033+
1034+
static uint ReadUInt32 (byte [] data, int offset) =>
1035+
(uint) (
1036+
data [offset] |
1037+
data [offset + 1] << 8 |
1038+
data [offset + 2] << 16 |
1039+
data [offset + 3] << 24);
1040+
8871041
static void AssertNoExportOutputs (ProjectBuilder builder, string memberName)
8881042
{
8891043
var typemapDirectory = builder.Output.GetIntermediaryPath ("typemap");

0 commit comments

Comments
 (0)