From c3540f0549c017ec25c862ef09c8eaca64d10763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Tue, 6 Oct 2026 19:52:56 +0200 Subject: [PATCH] fix(asm): read the riscv64 raw-data immediates at full width WORD and BYTE read their immediate through the truncating helper, so the int32 wrap turned WORD $0xffffffff into -1 and rejected it, while WORD $0x100000000 and BYTE $0x100000001 arrived pre-truncated and slipped past the range check as small values. Both statements now read the immediate the source wrote and bound it at the toolchain's own limits: [0, 0xffffffff] for WORD, [0, 0xff] for BYTE, with the bounds pinned in a test. Assisted-by: GLM 5.3 Flash --- asm/riscv_assemble.go | 29 +++++++++++++++++++++--- asm/riscv_encode_test.go | 49 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go index b31b9a5..60c11ec 100644 --- a/asm/riscv_assemble.go +++ b/asm/riscv_assemble.go @@ -1042,11 +1042,15 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv } return nil, nil case "WORD": - // WORD $w lays down a raw 32-bit little-endian word. + // WORD $w lays down a raw 32-bit little-endian word, in the range + // [0, 0xffffffff] exactly as the toolchain's validation bounds it. if len(ops) != 1 { return nil, fmt.Errorf("WORD expects 1 operand, got %d", len(ops)) } - w := int64(immFromOperand(ops[0])) + w, ok := riscvRawImm(ops[0]) + if !ok { + return nil, fmt.Errorf("WORD expects an immediate") + } if w < 0 || w > 0xFFFFFFFF { return nil, fmt.Errorf("WORD: immediate %d does not fit a 32-bit word", w) } @@ -1055,7 +1059,10 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv // BYTE $b lays down one raw byte per operand. var out []byte for _, op := range ops { - b := int64(immFromOperand(op)) + b, ok := riscvRawImm(op) + if !ok { + return nil, fmt.Errorf("BYTE expects immediates") + } if b < 0 || b > 0xFF { return nil, fmt.Errorf("BYTE: immediate %d does not fit a byte", b) } @@ -3249,6 +3256,22 @@ func immFromOperand(op *ast.Operand) int32 { return 0 } +// riscvRawImm reads an immediate at its full written width: the raw-data +// statements (WORD, BYTE) validate against their own ranges, so a value the +// source spelled wider than int32 must reach the check whole, never truncated +// through an int32 read (WORD $0xffffffff is in range, WORD $0x100000000 is +// not, and neither may arrive disguised as the other). +func riscvRawImm(op *ast.Operand) (int64, bool) { + if !op.Imm.HasVal { + return 0, false + } + v := op.Imm.Val + if op.Imm.Neg { + v = -v + } + return v, true +} + // riscvImm32FromOperand reads an immediate for the MOV/I-type paths as a // signed 32-bit value. The toolchain materialises wider constants through // its SLLI expansion, which this assembler does not implement, so values diff --git a/asm/riscv_encode_test.go b/asm/riscv_encode_test.go index 68f4384..02eb2ca 100644 --- a/asm/riscv_encode_test.go +++ b/asm/riscv_encode_test.go @@ -1340,3 +1340,52 @@ TEXT ·v(SB), NOSPLIT, $0 0xEA056207, // vlsseg8e32.v v4, (x10), x0 ) } + +// TestRISCV_rawDataRange pins the WORD and BYTE immediate ranges at the +// toolchain's own boundaries: WORD takes [0, 0xffffffff] and BYTE [0, 0xff], +// and a value the source spells wider must reach the check whole. The read +// once truncated through int32, which rejected WORD $0xffffffff as -1 while +// accepting WORD $0x100000000 as 0. +func TestRISCV_rawDataRange(t *testing.T) { + asmOne := func(t *testing.T, stmt string) ([]byte, error) { + t.Helper() + fn := firstTextRISCV(t, "#include \"textflag.h\"\nTEXT ·w(SB), NOSPLIT, $0\n\t"+stmt+"\n\tRET\n") + code, _, _, _, _, _, err := assembleRISCV(fn) + return code, err + } + t.Run("word bounds", func(t *testing.T) { + code, err := asmOne(t, "WORD $0") + if err != nil { + t.Fatalf("WORD $0: %v", err) + } + if string(code[:4]) != "\x00\x00\x00\x00" { + t.Errorf("WORD $0 = % x", code[:4]) + } + code, err = asmOne(t, "WORD $0xffffffff") + if err != nil { + t.Fatalf("WORD $0xffffffff must assemble, as go tool asm accepts it: %v", err) + } + if string(code[:4]) != "\xff\xff\xff\xff" { + t.Errorf("WORD $0xffffffff = % x", code[:4]) + } + for _, w := range []string{"$-1", "$0x100000000", "$-4294967296"} { + if _, err := asmOne(t, "WORD "+w); err == nil { + t.Errorf("WORD %s must be rejected, as go tool asm rejects it", w) + } + } + }) + t.Run("byte bounds", func(t *testing.T) { + code, err := asmOne(t, "BYTE $255") + if err != nil { + t.Fatalf("BYTE $255: %v", err) + } + if code[0] != 0xff { + t.Errorf("BYTE $255 = % x", code[:1]) + } + for _, b := range []string{"$256", "$0x100000001", "$-1"} { + if _, err := asmOne(t, "BYTE "+b); err == nil { + t.Errorf("BYTE %s must be rejected: out of the byte range", b) + } + } + }) +}