Skip to content

Commit 2c0c9b0

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 c79885b commit 2c0c9b0

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
@@ -1112,6 +1112,160 @@ public void DexUtils_RejectsDex041Containers ()
11121112
Assert.Throws<NotSupportedException> (() => DexUtils.GetClassDescriptors (dexFile));
11131113
}
11141114

1115+
[TestCase ("magic-terminator")]
1116+
[TestCase ("header-size")]
1117+
[TestCase ("file-size")]
1118+
[TestCase ("map-missing")]
1119+
[TestCase ("map-section")]
1120+
[TestCase ("string-section")]
1121+
[TestCase ("data-section")]
1122+
public void DexUtils_RejectsMalformedHeaders (string malformedField)
1123+
{
1124+
var dex = CreateMinimalDex ([(byte) 'L', (byte) 'x', (byte) ';'], utf16Length: 3);
1125+
switch (malformedField) {
1126+
case "magic-terminator":
1127+
dex [7] = 1;
1128+
break;
1129+
case "header-size":
1130+
WriteUInt32 (dex, 36, 111);
1131+
break;
1132+
case "file-size":
1133+
WriteUInt32 (dex, 32, checked ((uint) dex.Length - 1));
1134+
break;
1135+
case "map-missing":
1136+
WriteUInt32 (dex, 52, 0);
1137+
break;
1138+
case "map-section":
1139+
WriteUInt32 (dex, checked ((int) ReadUInt32 (dex, 52)), uint.MaxValue);
1140+
break;
1141+
case "string-section":
1142+
WriteUInt32 (dex, 56, uint.MaxValue);
1143+
break;
1144+
case "data-section":
1145+
WriteUInt32 (dex, 104, uint.MaxValue);
1146+
break;
1147+
default:
1148+
throw new InvalidOperationException ($"Unknown malformed DEX field '{malformedField}'.");
1149+
}
1150+
1151+
var dexFile = WriteDexFile (dex, malformedField);
1152+
Assert.Throws<InvalidDataException> (() => DexUtils.GetClassDescriptors (dexFile));
1153+
}
1154+
1155+
[Test]
1156+
public void DexUtils_DecodesModifiedUtf8 ()
1157+
{
1158+
var bmpDex = CreateMinimalDex (
1159+
[(byte) 'L', 0xc2, 0x80, 0xe0, 0xa0, 0x80, (byte) ';'],
1160+
utf16Length: 4);
1161+
var nullDex = CreateMinimalDex (
1162+
[(byte) 'L', 0xc0, 0x80, (byte) ';'],
1163+
utf16Length: 3);
1164+
var supplementaryDex = CreateMinimalDex (
1165+
[(byte) 'L', (byte) 'x', (byte) '/', 0xed, 0xa0, 0x81, 0xed, 0xb0, 0x80, (byte) ';'],
1166+
utf16Length: 6);
1167+
1168+
Assert.AreEqual ("L\u0080\u0800;", DexUtils.GetClassDescriptors (WriteDexFile (bmpDex, "bmp")).Single ());
1169+
Assert.AreEqual ("L\0;", DexUtils.GetClassDescriptors (WriteDexFile (nullDex, "null")).Single ());
1170+
Assert.AreEqual ("Lx/\U00010400;", DexUtils.GetClassDescriptors (WriteDexFile (supplementaryDex, "supplementary")).Single ());
1171+
}
1172+
1173+
[Test]
1174+
public void DexUtils_RejectsMalformedModifiedUtf8 ()
1175+
{
1176+
foreach (var (name, bytes, length) in new [] {
1177+
("raw-null", new byte [] { (byte) 'L', 0, (byte) 'x', (byte) ';' }, 4u),
1178+
("two-byte-overlong", new byte [] { (byte) 'L', 0xc0, 0x81, (byte) ';' }, 3u),
1179+
("two-byte-overlong-c1", new byte [] { (byte) 'L', 0xc1, 0xbf, (byte) ';' }, 3u),
1180+
("three-byte-overlong", new byte [] { (byte) 'L', 0xe0, 0x81, 0x81, (byte) ';' }, 3u),
1181+
("unmatched-high", new byte [] { (byte) 'L', 0xed, 0xa0, 0x81, (byte) ';' }, 3u),
1182+
("unmatched-low", new byte [] { (byte) 'L', 0xed, 0xb0, 0x80, (byte) ';' }, 3u),
1183+
}) {
1184+
var dexFile = WriteDexFile (CreateMinimalDex (bytes, length), name);
1185+
Assert.Throws<InvalidDataException> (() => DexUtils.GetClassDescriptors (dexFile));
1186+
}
1187+
}
1188+
1189+
[Test]
1190+
public void DexUtils_RejectsUnboundedDeclaredStringLength ()
1191+
{
1192+
var dex = CreateMinimalDex ([], uint.MaxValue);
1193+
var dexFile = WriteDexFile (dex, "huge-string");
1194+
1195+
Assert.Throws<InvalidDataException> (() => DexUtils.GetClassDescriptors (dexFile));
1196+
}
1197+
1198+
string WriteDexFile (byte [] dex, string name)
1199+
{
1200+
var directory = Path.Combine (Root, "temp", TestName);
1201+
Directory.CreateDirectory (directory);
1202+
var path = Path.Combine (directory, name + ".dex");
1203+
File.WriteAllBytes (path, dex);
1204+
return path;
1205+
}
1206+
1207+
static byte [] CreateMinimalDex (byte [] modifiedUtf8, uint utf16Length)
1208+
{
1209+
var length = EncodeUnsignedLeb128 (utf16Length);
1210+
const int stringIdsOffset = 112;
1211+
const int typeIdsOffset = 116;
1212+
const int classDefsOffset = 120;
1213+
const int stringDataOffset = 152;
1214+
int mapOffset = stringDataOffset + length.Length + modifiedUtf8.Length + 1;
1215+
var dex = new byte [mapOffset + 16];
1216+
Encoding.ASCII.GetBytes ("dex\n039\0").CopyTo (dex, 0);
1217+
WriteUInt32 (dex, 32, checked ((uint) dex.Length));
1218+
WriteUInt32 (dex, 36, 112);
1219+
WriteUInt32 (dex, 40, 0x12345678);
1220+
WriteUInt32 (dex, 52, checked ((uint) mapOffset));
1221+
WriteUInt32 (dex, 56, 1);
1222+
WriteUInt32 (dex, 60, stringIdsOffset);
1223+
WriteUInt32 (dex, 64, 1);
1224+
WriteUInt32 (dex, 68, typeIdsOffset);
1225+
WriteUInt32 (dex, 96, 1);
1226+
WriteUInt32 (dex, 100, classDefsOffset);
1227+
WriteUInt32 (dex, 104, checked ((uint) (dex.Length - stringDataOffset)));
1228+
WriteUInt32 (dex, 108, stringDataOffset);
1229+
WriteUInt32 (dex, stringIdsOffset, stringDataOffset);
1230+
length.CopyTo (dex, stringDataOffset);
1231+
modifiedUtf8.CopyTo (dex, stringDataOffset + length.Length);
1232+
WriteUInt32 (dex, mapOffset, 1);
1233+
dex [mapOffset + 4] = 0x00;
1234+
dex [mapOffset + 5] = 0x10;
1235+
WriteUInt32 (dex, mapOffset + 8, 1);
1236+
WriteUInt32 (dex, mapOffset + 12, checked ((uint) mapOffset));
1237+
return dex;
1238+
}
1239+
1240+
static byte [] EncodeUnsignedLeb128 (uint value)
1241+
{
1242+
var bytes = new List<byte> ();
1243+
do {
1244+
byte next = (byte) (value & 0x7f);
1245+
value >>= 7;
1246+
if (value != 0) {
1247+
next |= 0x80;
1248+
}
1249+
bytes.Add (next);
1250+
} while (value != 0);
1251+
return bytes.ToArray ();
1252+
}
1253+
1254+
static void WriteUInt32 (byte [] data, int offset, uint value)
1255+
{
1256+
data [offset] = (byte) value;
1257+
data [offset + 1] = (byte) (value >> 8);
1258+
data [offset + 2] = (byte) (value >> 16);
1259+
data [offset + 3] = (byte) (value >> 24);
1260+
}
1261+
1262+
static uint ReadUInt32 (byte [] data, int offset) =>
1263+
(uint) (
1264+
data [offset] |
1265+
data [offset + 1] << 8 |
1266+
data [offset + 2] << 16 |
1267+
data [offset + 3] << 24);
1268+
11151269
static void AssertNoExportOutputs (ProjectBuilder builder, string memberName)
11161270
{
11171271
var typemapDirectory = builder.Output.GetIntermediaryPath ("typemap");

0 commit comments

Comments
 (0)