From e8c84f0ba1a48d204dc510dc76fea19537d09448 Mon Sep 17 00:00:00 2001 From: Kevin Bataille Date: Wed, 22 Jul 2026 11:43:48 +0200 Subject: [PATCH] Fix IcedAssembler immediate overflow and unwrap invoke exceptions - TryBind wraps Convert.ChangeType in TryChangeType so an immediate that overflows a candidate parameter type returns false (letting a wider overload be tried) instead of throwing OverflowException out of assembly. Verified: "mov eax, 4294967295" and "mov eax, -2147483649" no longer crash. - Immediate now carries a boxed long OR ulong; TryParseImmediate parses decimal values above long.MaxValue via a ulong fallback, and hex via ulong. Previously such literals were rejected at parse time. - Unwrap TargetInvocationException from method.Invoke so callers see the real Iced failure, not the reflection wrapper. Tests: 231 passing, 4 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) --- WhiteMagic/Assembly/IcedAssembler.cs | 82 +++++++++++++++---- WhiteMagicTest/Assembly/IcedAssemblerTests.cs | 26 ++++++ 2 files changed, 94 insertions(+), 14 deletions(-) diff --git a/WhiteMagic/Assembly/IcedAssembler.cs b/WhiteMagic/Assembly/IcedAssembler.cs index cdacdb8..24aba34 100644 --- a/WhiteMagic/Assembly/IcedAssembler.cs +++ b/WhiteMagic/Assembly/IcedAssembler.cs @@ -129,7 +129,16 @@ public sealed class IcedAssembler : IAssembler if (TryBind(parameters, operands, out object?[]? boundArgs)) { - method.Invoke(assembler, boundArgs); + try + { + method.Invoke(assembler, boundArgs); + } + catch (TargetInvocationException ex) when (ex.InnerException is not null) + { + // Surface the real Iced failure rather than the reflection wrapper. + throw ex.InnerException; + } + return; } } @@ -151,7 +160,16 @@ public sealed class IcedAssembler : IAssembler switch (operand) { case Immediate imm when IsNumeric(paramType): - args[i] = Convert.ChangeType(imm.Value, paramType, CultureInfo.InvariantCulture); + // An immediate that overflows this parameter's type means this overload + // is the wrong width; return false so a wider overload can be tried + // instead of crashing the whole assembly. + if (!TryChangeType(imm.Value, paramType, out object? converted)) + { + boundArgs = null; + return false; + } + + args[i] = converted; break; case not null when paramType.IsInstanceOfType(operand): @@ -178,28 +196,63 @@ public sealed class IcedAssembler : IAssembler if (labels.TryGetValue(token, out Label label)) return label; - if (TryParseImmediate(token, out long value)) - return new Immediate(value); + if (TryParseImmediate(token, out object? value)) + return new Immediate(value!); throw new NotSupportedException( $"Unrecognized operand '{token}' (expected a register, an immediate, or a label)."); } - private static bool TryParseImmediate(string token, out long value) + // Parses an immediate as the narrowest of long/ulong that holds it, boxed. Storing the + // widest representation lets TryChangeType later narrow it to whatever integer parameter + // the chosen overload expects — and reject (rather than crash on) values that do not fit. + private static bool TryParseImmediate(string token, out object? value) { + value = null; bool negative = token.StartsWith('-'); string body = negative ? token[1..] : token; - bool ok; if (body.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) - ok = long.TryParse(body[2..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value); + { + if (!ulong.TryParse(body[2..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong hex)) + return false; + + value = negative ? -(long)hex : hex; + return true; + } + + if (negative) + { + if (!long.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out long signed)) + return false; + + value = signed; + return true; + } + + // Non-negative decimal: prefer long, fall back to ulong for values above long.MaxValue. + if (long.TryParse(body, NumberStyles.Integer, CultureInfo.InvariantCulture, out long asLong)) + value = asLong; + else if (ulong.TryParse(body, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong asULong)) + value = asULong; else - ok = long.TryParse(body, NumberStyles.Integer, CultureInfo.InvariantCulture, out value); + return false; - if (ok && negative) - value = -value; + return true; + } - return ok; + private static bool TryChangeType(object value, Type targetType, out object? result) + { + try + { + result = Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture); + return true; + } + catch (Exception ex) when (ex is OverflowException or InvalidCastException or FormatException) + { + result = null; + return false; + } } private static bool IsNumeric(Type type) => Type.GetTypeCode(type) is @@ -266,9 +319,10 @@ public sealed class IcedAssembler : IAssembler return map; } - // A parsed immediate, distinguished from register/label operands so binding can widen - // or narrow it to whichever integer parameter type the chosen overload expects. - private readonly record struct Immediate(long Value); + // A parsed immediate (boxed long or ulong), distinguished from register/label operands + // so binding can narrow it to whichever integer parameter type the chosen overload + // expects — or reject it when it does not fit. + private readonly record struct Immediate(object Value); private sealed class ByteListCodeWriter : CodeWriter { diff --git a/WhiteMagicTest/Assembly/IcedAssemblerTests.cs b/WhiteMagicTest/Assembly/IcedAssemblerTests.cs index 612c195..8350b05 100644 --- a/WhiteMagicTest/Assembly/IcedAssemblerTests.cs +++ b/WhiteMagicTest/Assembly/IcedAssemblerTests.cs @@ -82,6 +82,32 @@ public class IcedAssemblerTests Assert.Equal(expected, jmp.NearBranchTarget); } + [Theory] + [InlineData("mov eax, 4294967295")] // 0xFFFFFFFF — needs the uint overload, not int + [InlineData("mov eax, 0xFFFFFFFF")] // same value, hex form + [InlineData("mov rax, 18446744073709551615")] // ulong.MaxValue — decimal above long.MaxValue + public void Assemble_binds_wide_unsigned_immediates(string source) + { + var assembler = new IcedAssembler(64); + + byte[] code = assembler.Assemble(source); + Assert.NotEmpty(code); + + Instruction[] instructions = Disassemble(code, 64, 0); + Assert.Single(instructions); + Assert.Equal(Mnemonic.Mov, instructions[0].Mnemonic); + } + + [Fact] + public void Assemble_rejects_immediate_that_fits_no_overload_without_crashing() + { + var assembler = new IcedAssembler(64); + + // -2147483649 is below int.MinValue and eax has no wider signed overload; must be a + // clean NotSupportedException, not an OverflowException escaping from ChangeType. + Assert.Throws(() => assembler.Assemble("mov eax, -2147483649")); + } + [Fact] public void Assemble_throws_on_unsupported_operand() {