up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-27 07:46:56 +02:00
parent d63af51f84
commit ea970ead2a
302 changed files with 43161 additions and 1534 deletions

View File

@@ -0,0 +1,330 @@
using System.Buffers.Binary;
using System.Text;
namespace StellaOps.Scanner.Analyzers.Native;
/// <summary>
/// Parses Mach-O load commands to extract dependencies, rpaths, and UUIDs.
/// </summary>
public static class MachOLoadCommandParser
{
// Mach-O magic numbers
private const uint MH_MAGIC = 0xFEEDFACE; // 32-bit little endian
private const uint MH_CIGAM = 0xCEFAEDFE; // 32-bit big endian
private const uint MH_MAGIC_64 = 0xFEEDFACF; // 64-bit little endian
private const uint MH_CIGAM_64 = 0xCFFAEDFE; // 64-bit big endian
private const uint FAT_MAGIC = 0xCAFEBABE; // Fat binary big endian
private const uint FAT_CIGAM = 0xBEBAFECA; // Fat binary little endian
// Load commands
private const uint LC_LOAD_DYLIB = 0x0C;
private const uint LC_LOAD_WEAK_DYLIB = 0x80000018;
private const uint LC_REEXPORT_DYLIB = 0x8000001F;
private const uint LC_LAZY_LOAD_DYLIB = 0x20;
private const uint LC_RPATH = 0x8000001C;
private const uint LC_UUID = 0x1B;
/// <summary>
/// Parses Mach-O load commands from a stream.
/// </summary>
public static bool TryParse(Stream stream, out MachOImportInfo importInfo, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(stream);
importInfo = new MachOImportInfo(false, []);
using var buffer = new MemoryStream();
stream.CopyTo(buffer);
var data = buffer.ToArray();
var span = data.AsSpan();
if (span.Length < 4)
{
return false;
}
var magic = BinaryPrimitives.ReadUInt32BigEndian(span);
// Check for fat binary
if (magic == FAT_MAGIC || magic == FAT_CIGAM)
{
return TryParseFatBinary(span, magic == FAT_CIGAM, out importInfo);
}
// Check for single architecture binary
if (magic == MH_MAGIC || magic == MH_CIGAM || magic == MH_MAGIC_64 || magic == MH_CIGAM_64)
{
if (TryParseSingleArchitecture(span, 0, out var slice))
{
importInfo = new MachOImportInfo(false, [slice]);
return true;
}
}
return false;
}
private static bool TryParseFatBinary(ReadOnlySpan<byte> span, bool swapped, out MachOImportInfo importInfo)
{
importInfo = new MachOImportInfo(true, []);
if (span.Length < 8)
{
return false;
}
var nfat_arch = swapped
? BinaryPrimitives.ReverseEndianness(BinaryPrimitives.ReadUInt32BigEndian(span.Slice(4, 4)))
: BinaryPrimitives.ReadUInt32BigEndian(span.Slice(4, 4));
if (nfat_arch > 20) // Sanity check
{
return false;
}
var slices = new List<MachOSlice>();
var fatArchSize = 20; // sizeof(fat_arch) for 32-bit fat header
var offset = 8;
for (var i = 0; i < nfat_arch; i++)
{
if (offset + fatArchSize > span.Length)
{
break;
}
var archOffset = swapped
? BinaryPrimitives.ReverseEndianness(BinaryPrimitives.ReadUInt32BigEndian(span.Slice(offset + 8, 4)))
: BinaryPrimitives.ReadUInt32BigEndian(span.Slice(offset + 8, 4));
if (archOffset < span.Length && TryParseSingleArchitecture(span, (int)archOffset, out var slice))
{
slices.Add(slice);
}
offset += fatArchSize;
}
importInfo = new MachOImportInfo(true, slices);
return slices.Count > 0;
}
private static bool TryParseSingleArchitecture(ReadOnlySpan<byte> span, int baseOffset, out MachOSlice slice)
{
slice = new MachOSlice(null, 0, null, [], []);
if (baseOffset + 28 > span.Length)
{
return false;
}
var localSpan = span[baseOffset..];
var magic = BinaryPrimitives.ReadUInt32LittleEndian(localSpan);
bool is64Bit;
bool bigEndian;
switch (magic)
{
case MH_MAGIC:
is64Bit = false;
bigEndian = false;
break;
case MH_CIGAM:
is64Bit = false;
bigEndian = true;
break;
case MH_MAGIC_64:
is64Bit = true;
bigEndian = false;
break;
case MH_CIGAM_64:
is64Bit = true;
bigEndian = true;
break;
default:
return false;
}
// Parse mach_header(_64)
var cputype = ReadUInt32(localSpan, 4, bigEndian);
var cpusubtype = ReadUInt32(localSpan, 8, bigEndian);
var ncmds = ReadUInt32(localSpan, 16, bigEndian);
var sizeofcmds = ReadUInt32(localSpan, 20, bigEndian);
var headerSize = is64Bit ? 32 : 28;
var cmdOffset = headerSize;
string? uuid = null;
var rpaths = new List<string>();
var dependencies = new List<MachODeclaredDependency>();
var seenPaths = new HashSet<string>(StringComparer.Ordinal);
for (var i = 0; i < ncmds; i++)
{
if (cmdOffset + 8 > localSpan.Length)
{
break;
}
var cmd = ReadUInt32(localSpan, cmdOffset, bigEndian);
var cmdsize = ReadUInt32(localSpan, cmdOffset + 4, bigEndian);
if (cmdsize < 8 || cmdOffset + cmdsize > localSpan.Length)
{
break;
}
switch (cmd)
{
case LC_UUID when cmdsize >= 24:
uuid = ParseUuid(localSpan.Slice(cmdOffset + 8, 16));
break;
case LC_RPATH:
var rpathStr = ParseLoadCommandString(localSpan, cmdOffset, cmdsize, bigEndian);
if (!string.IsNullOrEmpty(rpathStr))
{
rpaths.Add(rpathStr);
}
break;
case LC_LOAD_DYLIB:
AddDependency(localSpan, cmdOffset, cmdsize, bigEndian, "macho-loadlib", seenPaths, dependencies);
break;
case LC_LOAD_WEAK_DYLIB:
AddDependency(localSpan, cmdOffset, cmdsize, bigEndian, "macho-weaklib", seenPaths, dependencies);
break;
case LC_REEXPORT_DYLIB:
AddDependency(localSpan, cmdOffset, cmdsize, bigEndian, "macho-reexport", seenPaths, dependencies);
break;
case LC_LAZY_LOAD_DYLIB:
AddDependency(localSpan, cmdOffset, cmdsize, bigEndian, "macho-lazylib", seenPaths, dependencies);
break;
}
cmdOffset += (int)cmdsize;
}
var cpuTypeStr = MapCpuType(cputype);
slice = new MachOSlice(cpuTypeStr, cpusubtype, uuid, rpaths, dependencies);
return true;
}
private static void AddDependency(
ReadOnlySpan<byte> span, int cmdOffset, uint cmdsize, bool bigEndian,
string reasonCode, HashSet<string> seenPaths, List<MachODeclaredDependency> dependencies)
{
// dylib_command structure:
// uint32_t cmd, cmdsize
// lc_str name (offset from start of load command)
// uint32_t timestamp
// uint32_t current_version
// uint32_t compatibility_version
if (cmdsize < 24)
{
return;
}
var nameOffset = ReadUInt32(span, cmdOffset + 8, bigEndian);
var currentVersion = ReadUInt32(span, cmdOffset + 16, bigEndian);
var compatVersion = ReadUInt32(span, cmdOffset + 20, bigEndian);
var pathEnd = (int)Math.Min(cmdsize, (uint)(span.Length - cmdOffset));
var nameStart = cmdOffset + (int)nameOffset;
if (nameStart >= span.Length || nameStart >= cmdOffset + pathEnd)
{
return;
}
var nameSpan = span.Slice(nameStart, Math.Min(pathEnd - (int)nameOffset, span.Length - nameStart));
var nullIndex = nameSpan.IndexOf((byte)0);
var nameLength = nullIndex >= 0 ? nullIndex : nameSpan.Length;
var path = Encoding.UTF8.GetString(nameSpan[..nameLength]);
if (string.IsNullOrEmpty(path) || !seenPaths.Add(path))
{
return;
}
var currentVersionStr = FormatVersion(currentVersion);
var compatVersionStr = FormatVersion(compatVersion);
dependencies.Add(new MachODeclaredDependency(path, reasonCode, currentVersionStr, compatVersionStr));
}
private static string? ParseLoadCommandString(ReadOnlySpan<byte> span, int cmdOffset, uint cmdsize, bool bigEndian)
{
// LC_RPATH structure:
// uint32_t cmd, cmdsize
// lc_str path (offset from start of load command)
if (cmdsize < 12)
{
return null;
}
var pathOffset = ReadUInt32(span, cmdOffset + 8, bigEndian);
var pathEnd = (int)Math.Min(cmdsize, (uint)(span.Length - cmdOffset));
var pathStart = cmdOffset + (int)pathOffset;
if (pathStart >= span.Length || pathStart >= cmdOffset + pathEnd)
{
return null;
}
var pathSpan = span.Slice(pathStart, Math.Min(pathEnd - (int)pathOffset, span.Length - pathStart));
var nullIndex = pathSpan.IndexOf((byte)0);
var pathLength = nullIndex >= 0 ? nullIndex : pathSpan.Length;
return Encoding.UTF8.GetString(pathSpan[..pathLength]);
}
private static string ParseUuid(ReadOnlySpan<byte> uuidBytes)
{
// Format as standard UUID: 8-4-4-4-12 (all lowercase)
return ($"{Convert.ToHexString(uuidBytes[..4])}-{Convert.ToHexString(uuidBytes.Slice(4, 2))}-" +
$"{Convert.ToHexString(uuidBytes.Slice(6, 2))}-{Convert.ToHexString(uuidBytes.Slice(8, 2))}-" +
$"{Convert.ToHexString(uuidBytes.Slice(10, 6))}").ToLowerInvariant();
}
private static string FormatVersion(uint version)
{
// Mach-O version format: xxxx.yy.zz encoded as (xxxx << 16) | (yy << 8) | zz
var major = version >> 16;
var minor = (version >> 8) & 0xFF;
var patch = version & 0xFF;
return $"{major}.{minor}.{patch}";
}
private static uint ReadUInt32(ReadOnlySpan<byte> span, int offset, bool bigEndian)
{
return bigEndian
? BinaryPrimitives.ReadUInt32BigEndian(span.Slice(offset, 4))
: BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(offset, 4));
}
private static string? MapCpuType(uint cpuType) => cpuType switch
{
0x00000001 => "vax",
0x00000006 => "mc680x0",
0x00000007 => "x86",
0x01000007 => "x86_64",
0x0000000A => "mc98000",
0x0000000B => "hppa",
0x0000000C => "arm",
0x0100000C => "arm64",
0x0200000C => "arm64_32",
0x0000000D => "mc88000",
0x0000000E => "sparc",
0x0000000F => "i860",
0x00000012 => "powerpc",
0x01000012 => "powerpc64",
_ => null,
};
}