← board

A class-reference operation name hijacks a record field

Filed by the Track T watcher as regression-test-core-test-rtti and regression-test-core-test-classref (both Segmentation fault, bad 392ea5d94545, last good 766e6ea5b4d6). Re-verified at HEAD before acting; both still red.

Repro

program rt1;
uses typinfo;
type TB = class private FId: Integer; published property Id: Integer read FId write FId; end;
var cls: PClassRTTI;
begin
  cls := GetClass('TB');
  Writeln('name: ', cls^.NamePtr^);      { TB   }
  Writeln('size: ', cls^.InstanceSize);  { SIGSEGV }
end.

PXXDBG=a.ir on the routine shows it plainly — instead of a field read at offset 16 it emits load_mem, load_mem, +8, load_mem, then a call to __pxxInstanceSize: the RECORD's first word was read as an object's class pointer.

Root cause

5cdebf0f5 (TObject.InstanceSize and ClassNameIs) added two names to IsClassRefOpName. The arm that answers them on an INSTANCE receiver (pasparser_lval.inc, "obj.ClassName / obj.ClassType / obj.InheritsFrom(C)") guarded only on

(mmi < 0) and (mci >= 0) and IsClassRefOpName(fieldName)

— no METHOD of that name, and the receiver is a class-LIKE entry. Records are class-like entries too (UClsIsRecord), and nothing asked about FIELDS.

That was harmless while the set was ClassName / ClassType / InheritsFrom / ClassParent: nobody names a record field any of those. InstanceSize is a real field of typinfo.TClassRTTI — the very record the RTTI API hands out — so the new name landed exactly on the one collision the guard could not survive.

The two SIBLING sites already made both tests: the chained-value arm ((x as T).ClassName) checks FindUField(ci, ...) < 0 and not UClsIsRecord[ci], and the implicit-Self arm in pasparser_expr.inc checks FindUField too. This arm was the one nobody went back for — the double-case shape devdocs/dev/normalise-dont-special-case.md is about.

The fix

Add the two tests this arm was missing, so all three sites ask the same question: a record receiver never reaches a class-reference operation, and a real field of the name outranks it.

The siblings, checked before closing

The two neighbouring arms in the same function — obj.GetInterface(IID, Obj) and obj.MethodAddress(name) / obj.MethodName(addr) — carry the identical (mmi < 0) and (mci >= 0) guard with no record test. Both additionally require a following (, so no field READ can reach them today; a proc-typed record field called as r.MethodAddress(x) could. They are TObject operations and a record has none of them, so the exclusion is added there too rather than left as the next instance of this bug.

Verified

test/test_rtti.pas and test/test_classref.pas both run to completion again (28 and 3 lines, previously SIGSEGV after 2). New focused test test/test_a_record_field_named_like_a_class_operation.pas, byte-identical to fpc -Mobjfpc -O1: all four colliding names as record fields, read through the variable AND through a pointer (the spelling that crashed), a write through the pointer landing in the field, and a class with no member of the name still reaching the operation — the row that proves the guard did not simply switch the arm off.

Not asserted: a CLASS field of one of these names. FPC rejects that outright (Duplicate identifier "InstanceSize"), so there is no oracle for the row; pxx accepts it and the fix makes the field win, which is the sane direction.

Gate

make compiler/pascal26 (self-host fixedpoint) + tools/gate.sh quick GREEN.