Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,44 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Reflection;
internal sealed record JavaReflectionAnalysis(
ImmutableArray<JavaReflectionEdge> Edges,
ImmutableArray<JavaReflectionWarning> Warnings)
{
public static readonly JavaReflectionAnalysis Empty = new(ImmutableArray<JavaReflectionEdge>.Empty, ImmutableArray<JavaReflectionWarning>.Empty);
}
internal sealed record JavaReflectionEdge(
string SourceClass,
string SegmentIdentifier,
string? TargetType,
JavaReflectionReason Reason,
JavaReflectionConfidence Confidence,
string MethodName,
string MethodDescriptor,
int InstructionOffset,
string? Details);
internal sealed record JavaReflectionWarning(
string SourceClass,
string SegmentIdentifier,
string WarningCode,
string Message,
string MethodName,
string MethodDescriptor);
internal enum JavaReflectionReason
{
ClassForName,
ClassLoaderLoadClass,
ServiceLoaderLoad,
ResourceLookup,
}
internal enum JavaReflectionConfidence
{
Low = 1,
Medium = 2,
High = 3,
}

View File

@@ -0,0 +1,716 @@
using System.Buffers.Binary;
using System.Collections.Immutable;
using System.Text;
using StellaOps.Scanner.Analyzers.Lang.Java.Internal.ClassPath;
namespace StellaOps.Scanner.Analyzers.Lang.Java.Internal.Reflection;
internal static class JavaReflectionAnalyzer
{
public static JavaReflectionAnalysis Analyze(JavaClassPathAnalysis classPath, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(classPath);
if (classPath.Segments.IsDefaultOrEmpty)
{
return JavaReflectionAnalysis.Empty;
}
var edges = new List<JavaReflectionEdge>();
var warnings = new List<JavaReflectionWarning>();
foreach (var segment in classPath.Segments)
{
cancellationToken.ThrowIfCancellationRequested();
foreach (var kvp in segment.ClassLocations)
{
var className = kvp.Key;
var location = kvp.Value;
using var stream = location.OpenClassStream(cancellationToken);
var classFile = JavaClassFile.Parse(stream, cancellationToken);
foreach (var method in classFile.Methods)
{
cancellationToken.ThrowIfCancellationRequested();
AnalyzeMethod(classFile, method, segment.Identifier, className, edges, warnings);
}
}
}
if (edges.Count == 0 && warnings.Count == 0)
{
return JavaReflectionAnalysis.Empty;
}
return new JavaReflectionAnalysis(
edges.ToImmutableArray(),
warnings.ToImmutableArray());
}
private static void AnalyzeMethod(
JavaClassFile classFile,
JavaMethod method,
string segmentIdentifier,
string className,
List<JavaReflectionEdge> edges,
List<JavaReflectionWarning> warnings)
{
var pool = classFile.ConstantPool;
string? pendingStringLiteral = null;
string? pendingClassLiteral = null;
var sawCurrentThread = false;
var emittedTcclWarning = false;
var code = method.Code;
var offset = 0;
var length = code.Length;
while (offset < length)
{
var instructionOffset = offset;
var opcode = code[offset++];
switch (opcode)
{
case 0x12: // LDC
{
var index = code[offset++];
HandleLdc(index, pool, ref pendingStringLiteral, ref pendingClassLiteral);
break;
}
case 0x13: // LDC_W
case 0x14: // LDC2_W
{
var index = (code[offset++] << 8) | code[offset++];
HandleLdc(index, pool, ref pendingStringLiteral, ref pendingClassLiteral);
break;
}
case 0xB8: // invokestatic
case 0xB6: // invokevirtual
case 0xB7: // invokespecial
case 0xB9: // invokeinterface
{
var methodIndex = (code[offset++] << 8) | code[offset++];
if (opcode == 0xB9)
{
offset += 2; // count and zero
}
HandleInvocation(
pool,
method,
segmentIdentifier,
className,
instructionOffset,
methodIndex,
opcode,
ref pendingStringLiteral,
ref pendingClassLiteral,
ref sawCurrentThread,
ref emittedTcclWarning,
edges,
warnings);
pendingStringLiteral = null;
pendingClassLiteral = null;
break;
}
default:
{
if (IsStoreInstruction(opcode))
{
pendingStringLiteral = null;
pendingClassLiteral = null;
if (IsStoreWithExplicitIndex(opcode))
{
offset++;
}
}
else if (IsLoadInstructionWithIndex(opcode))
{
offset++;
}
else if (IsStackMutation(opcode))
{
pendingStringLiteral = null;
pendingClassLiteral = null;
}
break;
}
}
}
// When the method calls Thread.currentThread without accessing the context loader, we do not emit warnings.
}
private static void HandleLdc(
int constantIndex,
JavaConstantPool pool,
ref string? pendingString,
ref string? pendingClassLiteral)
{
var constantKind = pool.GetConstantKind(constantIndex);
switch (constantKind)
{
case JavaConstantKind.String:
pendingString = pool.GetString(constantIndex);
pendingClassLiteral = null;
break;
case JavaConstantKind.Class:
pendingClassLiteral = pool.GetClassName(constantIndex);
pendingString = null;
break;
default:
pendingString = null;
pendingClassLiteral = null;
break;
}
}
private static void HandleInvocation(
JavaConstantPool pool,
JavaMethod method,
string segmentIdentifier,
string className,
int instructionOffset,
int methodIndex,
byte opcode,
ref string? pendingString,
ref string? pendingClassLiteral,
ref bool sawCurrentThread,
ref bool emittedTcclWarning,
List<JavaReflectionEdge> edges,
List<JavaReflectionWarning> warnings)
{
var methodRef = pool.GetMethodReference(methodIndex);
var owner = methodRef.OwnerInternalName;
var name = methodRef.Name;
var descriptor = methodRef.Descriptor;
var normalizedOwner = owner ?? string.Empty;
var normalizedSource = NormalizeClassName(className) ?? className ?? string.Empty;
if (normalizedOwner == "java/lang/Class" && name == "forName")
{
var target = NormalizeClassName(pendingString);
var confidence = pendingString is null ? JavaReflectionConfidence.Low : JavaReflectionConfidence.High;
edges.Add(new JavaReflectionEdge(
normalizedSource,
segmentIdentifier,
target,
JavaReflectionReason.ClassForName,
confidence,
method.Name,
method.Descriptor,
instructionOffset,
null));
}
else if (normalizedOwner == "java/lang/ClassLoader" && name == "loadClass")
{
var target = NormalizeClassName(pendingString);
var confidence = pendingString is null ? JavaReflectionConfidence.Low : JavaReflectionConfidence.High;
edges.Add(new JavaReflectionEdge(
normalizedSource,
segmentIdentifier,
target,
JavaReflectionReason.ClassLoaderLoadClass,
confidence,
method.Name,
method.Descriptor,
instructionOffset,
null));
}
else if (normalizedOwner == "java/util/ServiceLoader" && name.StartsWith("load", StringComparison.Ordinal))
{
var target = NormalizeClassName(pendingClassLiteral);
var confidence = pendingClassLiteral is null ? JavaReflectionConfidence.Low : JavaReflectionConfidence.High;
edges.Add(new JavaReflectionEdge(
normalizedSource,
segmentIdentifier,
target,
JavaReflectionReason.ServiceLoaderLoad,
confidence,
method.Name,
method.Descriptor,
instructionOffset,
null));
}
else if (normalizedOwner == "java/lang/ClassLoader" && (name == "getResource" || name == "getResourceAsStream" || name == "getResources"))
{
var target = pendingString;
var confidence = pendingString is null ? JavaReflectionConfidence.Low : JavaReflectionConfidence.High;
edges.Add(new JavaReflectionEdge(
normalizedSource,
segmentIdentifier,
target,
JavaReflectionReason.ResourceLookup,
confidence,
method.Name,
method.Descriptor,
instructionOffset,
null));
}
else if (normalizedOwner == "java/lang/Thread" && name == "currentThread")
{
sawCurrentThread = true;
}
else if (normalizedOwner == "java/lang/Thread" && name == "getContextClassLoader")
{
if (sawCurrentThread && !emittedTcclWarning)
{
warnings.Add(new JavaReflectionWarning(
normalizedSource,
segmentIdentifier,
"tccl",
"Thread context class loader access detected.",
method.Name,
method.Descriptor));
emittedTcclWarning = true;
}
}
pendingString = null;
pendingClassLiteral = null;
}
private static string? NormalizeClassName(string? internalName)
{
if (string.IsNullOrWhiteSpace(internalName))
{
return null;
}
return internalName.Replace('/', '.');
}
private static bool IsStoreInstruction(byte opcode)
=> (opcode >= 0x3B && opcode <= 0x4E) || (opcode >= 0x4F && opcode <= 0x56) || (opcode >= 0x36 && opcode <= 0x3A);
private static bool IsStoreWithExplicitIndex(byte opcode)
=> opcode >= 0x36 && opcode <= 0x3A;
private static bool IsLoadInstructionWithIndex(byte opcode)
=> opcode >= 0x15 && opcode <= 0x19;
private static bool IsStackMutation(byte opcode)
=> opcode is 0x57 or 0x58 or 0x59 or 0x5A or 0x5B or 0x5C or 0x5D or 0x5E or 0x5F;
private sealed class JavaClassFile
{
public JavaClassFile(string thisClassName, JavaConstantPool constantPool, ImmutableArray<JavaMethod> methods)
{
ThisClassName = thisClassName;
ConstantPool = constantPool;
Methods = methods;
}
public string ThisClassName { get; }
public JavaConstantPool ConstantPool { get; }
public ImmutableArray<JavaMethod> Methods { get; }
public static JavaClassFile Parse(Stream stream, CancellationToken cancellationToken)
{
var reader = new BigEndianReader(stream, leaveOpen: true);
if (reader.ReadUInt32() != 0xCAFEBABE)
{
throw new InvalidDataException("Invalid Java class file magic header.");
}
_ = reader.ReadUInt16(); // minor
_ = reader.ReadUInt16(); // major
var constantPoolCount = reader.ReadUInt16();
var pool = new JavaConstantPool(constantPoolCount);
var index = 1;
while (index < constantPoolCount)
{
cancellationToken.ThrowIfCancellationRequested();
var tag = reader.ReadByte();
switch ((JavaConstantTag)tag)
{
case JavaConstantTag.Utf8:
{
pool.Set(index, JavaConstantPoolEntry.Utf8(reader.ReadUtf8()));
index++;
break;
}
case JavaConstantTag.Integer:
reader.Skip(4);
pool.Set(index, JavaConstantPoolEntry.Other(tag));
index++;
break;
case JavaConstantTag.Float:
reader.Skip(4);
pool.Set(index, JavaConstantPoolEntry.Other(tag));
index++;
break;
case JavaConstantTag.Long:
case JavaConstantTag.Double:
reader.Skip(8);
pool.Set(index, JavaConstantPoolEntry.Other(tag));
index += 2;
break;
case JavaConstantTag.Class:
case JavaConstantTag.String:
case JavaConstantTag.MethodType:
pool.Set(index, JavaConstantPoolEntry.Indexed(tag, reader.ReadUInt16()));
index++;
break;
case JavaConstantTag.Fieldref:
case JavaConstantTag.Methodref:
case JavaConstantTag.InterfaceMethodref:
case JavaConstantTag.NameAndType:
case JavaConstantTag.InvokeDynamic:
pool.Set(index, JavaConstantPoolEntry.IndexedPair(tag, reader.ReadUInt16(), reader.ReadUInt16()));
index++;
break;
case JavaConstantTag.MethodHandle:
reader.Skip(1); // reference kind
pool.Set(index, JavaConstantPoolEntry.Indexed(tag, reader.ReadUInt16()));
index++;
break;
default:
throw new InvalidDataException($"Unsupported constant pool tag {tag}.");
}
}
var accessFlags = reader.ReadUInt16();
var thisClassIndex = reader.ReadUInt16();
_ = reader.ReadUInt16(); // super
var interfacesCount = reader.ReadUInt16();
reader.Skip(interfacesCount * 2);
var fieldsCount = reader.ReadUInt16();
for (var i = 0; i < fieldsCount; i++)
{
SkipMember(reader);
}
var methodsCount = reader.ReadUInt16();
var methods = ImmutableArray.CreateBuilder<JavaMethod>(methodsCount);
for (var i = 0; i < methodsCount; i++)
{
cancellationToken.ThrowIfCancellationRequested();
_ = reader.ReadUInt16(); // method access flags
var nameIndex = reader.ReadUInt16();
var descriptorIndex = reader.ReadUInt16();
var attributesCount = reader.ReadUInt16();
byte[]? code = null;
for (var attr = 0; attr < attributesCount; attr++)
{
var attributeNameIndex = reader.ReadUInt16();
var attributeLength = reader.ReadUInt32();
var attributeName = pool.GetUtf8(attributeNameIndex) ?? string.Empty;
if (attributeName == "Code")
{
var maxStack = reader.ReadUInt16();
var maxLocals = reader.ReadUInt16();
var codeLength = reader.ReadUInt32();
code = reader.ReadBytes((int)codeLength);
var exceptionTableLength = reader.ReadUInt16();
reader.Skip(exceptionTableLength * 8);
var codeAttributeCount = reader.ReadUInt16();
for (var c = 0; c < codeAttributeCount; c++)
{
reader.Skip(2); // name index
var len = reader.ReadUInt32();
reader.Skip((int)len);
}
}
else
{
reader.Skip((int)attributeLength);
}
}
if (code is not null)
{
var name = pool.GetUtf8(nameIndex) ?? string.Empty;
var descriptor = pool.GetUtf8(descriptorIndex) ?? string.Empty;
methods.Add(new JavaMethod(name, descriptor, code));
}
}
var classAttributesCount = reader.ReadUInt16();
for (var a = 0; a < classAttributesCount; a++)
{
reader.Skip(2);
var len = reader.ReadUInt32();
reader.Skip((int)len);
}
var thisClassName = pool.GetClassName(thisClassIndex) ?? string.Empty;
return new JavaClassFile(thisClassName, pool, methods.ToImmutable());
}
private static void SkipMember(BigEndianReader reader)
{
reader.Skip(6); // access, name, descriptor
var attributeCount = reader.ReadUInt16();
for (var i = 0; i < attributeCount; i++)
{
reader.Skip(2);
var len = reader.ReadUInt32();
reader.Skip((int)len);
}
}
}
private sealed class JavaMethod
{
public JavaMethod(string name, string descriptor, byte[] code)
{
Name = name;
Descriptor = descriptor;
Code = code;
}
public string Name { get; }
public string Descriptor { get; }
public byte[] Code { get; }
}
private sealed class JavaConstantPool
{
private readonly JavaConstantPoolEntry?[] _entries;
public JavaConstantPool(int count)
{
_entries = new JavaConstantPoolEntry?[count];
}
public void Set(int index, JavaConstantPoolEntry entry)
{
_entries[index] = entry;
}
public JavaConstantKind GetConstantKind(int index)
{
var entry = _entries[index];
return entry?.Kind ?? JavaConstantKind.Other;
}
public string? GetUtf8(int index)
{
if (index <= 0 || index >= _entries.Length)
{
return null;
}
return _entries[index] is JavaConstantPoolEntry.Utf8Entry utf8 ? utf8.Value : null;
}
public string? GetString(int index)
{
if (_entries[index] is JavaConstantPoolEntry.IndexedEntry { Kind: JavaConstantKind.String, Index: var utf8Index })
{
return GetUtf8(utf8Index);
}
return null;
}
public string? GetClassName(int index)
{
if (_entries[index] is JavaConstantPoolEntry.IndexedEntry { Kind: JavaConstantKind.Class, Index: var nameIndex })
{
return GetUtf8(nameIndex);
}
return null;
}
public JavaMethodReference GetMethodReference(int index)
{
if (_entries[index] is not JavaConstantPoolEntry.IndexedPairEntry pair || pair.Kind is not (JavaConstantKind.Methodref or JavaConstantKind.InterfaceMethodref))
{
throw new InvalidDataException($"Constant pool entry {index} is not a method reference.");
}
var owner = GetClassName(pair.FirstIndex) ?? string.Empty;
var nameAndType = _entries[pair.SecondIndex] as JavaConstantPoolEntry.IndexedPairEntry;
if (nameAndType is null || nameAndType.Kind != JavaConstantKind.NameAndType)
{
throw new InvalidDataException("Invalid NameAndType entry for method reference.");
}
var name = GetUtf8(nameAndType.FirstIndex) ?? string.Empty;
var descriptor = GetUtf8(nameAndType.SecondIndex) ?? string.Empty;
return new JavaMethodReference(owner, name, descriptor);
}
}
private readonly record struct JavaMethodReference(string OwnerInternalName, string Name, string Descriptor);
private abstract record JavaConstantPoolEntry(JavaConstantKind Kind)
{
public sealed record Utf8Entry(string Value) : JavaConstantPoolEntry(JavaConstantKind.Utf8);
public sealed record IndexedEntry(JavaConstantKind Kind, ushort Index) : JavaConstantPoolEntry(Kind);
public sealed record IndexedPairEntry(JavaConstantKind Kind, ushort FirstIndex, ushort SecondIndex) : JavaConstantPoolEntry(Kind);
public sealed record OtherEntry(byte Tag) : JavaConstantPoolEntry(JavaConstantKind.Other);
public static JavaConstantPoolEntry Utf8(string value) => new Utf8Entry(value);
public static JavaConstantPoolEntry Indexed(byte tag, ushort index)
=> new IndexedEntry(ToKind(tag), index);
public static JavaConstantPoolEntry IndexedPair(byte tag, ushort first, ushort second)
=> new IndexedPairEntry(ToKind(tag), first, second);
public static JavaConstantPoolEntry Other(byte tag) => new OtherEntry(tag);
private static JavaConstantKind ToKind(byte tag)
=> tag switch
{
7 => JavaConstantKind.Class,
8 => JavaConstantKind.String,
9 => JavaConstantKind.Fieldref,
10 => JavaConstantKind.Methodref,
11 => JavaConstantKind.InterfaceMethodref,
12 => JavaConstantKind.NameAndType,
15 => JavaConstantKind.MethodHandle,
16 => JavaConstantKind.MethodType,
18 => JavaConstantKind.InvokeDynamic,
_ => JavaConstantKind.Other,
};
}
private enum JavaConstantKind
{
Utf8,
Integer,
Float,
Long,
Double,
Class,
String,
Fieldref,
Methodref,
InterfaceMethodref,
NameAndType,
MethodHandle,
MethodType,
InvokeDynamic,
Other,
}
private enum JavaConstantTag : byte
{
Utf8 = 1,
Integer = 3,
Float = 4,
Long = 5,
Double = 6,
Class = 7,
String = 8,
Fieldref = 9,
Methodref = 10,
InterfaceMethodref = 11,
NameAndType = 12,
MethodHandle = 15,
MethodType = 16,
InvokeDynamic = 18,
}
private sealed class BigEndianReader
{
private readonly Stream _stream;
private readonly BinaryReader _reader;
public BigEndianReader(Stream stream, bool leaveOpen)
{
_stream = stream;
_reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen);
}
public ushort ReadUInt16()
{
Span<byte> buffer = stackalloc byte[2];
Fill(buffer);
return BinaryPrimitives.ReadUInt16BigEndian(buffer);
}
public uint ReadUInt32()
{
Span<byte> buffer = stackalloc byte[4];
Fill(buffer);
return BinaryPrimitives.ReadUInt32BigEndian(buffer);
}
public int ReadInt32()
{
Span<byte> buffer = stackalloc byte[4];
Fill(buffer);
return BinaryPrimitives.ReadInt32BigEndian(buffer);
}
public byte ReadByte() => _reader.ReadByte();
public string ReadUtf8()
{
var length = ReadUInt16();
var bytes = ReadBytes(length);
return Encoding.UTF8.GetString(bytes);
}
public byte[] ReadBytes(int count)
{
var bytes = _reader.ReadBytes(count);
if (bytes.Length != count)
{
throw new EndOfStreamException();
}
return bytes;
}
public void Skip(int count)
{
if (count <= 0)
{
return;
}
var buffer = new byte[Math.Min(count, 4096)];
var remaining = count;
while (remaining > 0)
{
var read = _stream.Read(buffer, 0, Math.Min(buffer.Length, remaining));
if (read == 0)
{
throw new EndOfStreamException();
}
remaining -= read;
}
}
private void Fill(Span<byte> buffer)
{
var read = _stream.Read(buffer);
if (read != buffer.Length)
{
throw new EndOfStreamException();
}
}
}
}