fix(codegen-native): address record/aggregate threadvars via TLS

A threadvar of record (or any aggregate whose base address is taken via
leaq) was addressed with a bare `leaq Name(%rip)` for both direct field
access and method-call receivers, with no %fs:0/@tpoff. The symbol was
correctly placed in .tbss, but a %rip-relative reference to a TLS symbol
does not resolve to the per-thread slot, so all threads shared one static
slot and clobbered each other. Scalar threadvars were already correct
(VarOperand emits %fs:Name@tpoff); QBE was unaffected.

Route every global record/array base load through the TLS-aware
EmitLeaqGlobal helper instead of bare `leaq Name(%rip)`:
  - 16 field-access base ladders (FAE/FA/AFA.RecordName)
  - 4 record-method receiver sites (the >6-slot and <=6-slot ladders)
  - the offset-0 scalar-field-read fast path, which read the field
    directly off Name(%rip); now branches on IsThreadVarGlobal and falls
    through to EmitLeaqGlobal + indirect load for threadvars.

EmitLeaqGlobal emits the identical bare leaq for non-threadvar globals,
so codegen for ordinary globals is byte-identical (verified by .s diff)
and all four fixpoints are unchanged.

Tests: cp.test.e2e.threading gains
TestRun_ThreadVar_RecordField_PerThreadIsolation and
TestRun_ThreadVar_RecordMethod_PerThreadIsolation — 3 threads each stamp
a distinct id into a record threadvar (via field access / via method
call), spin re-reading, and assert no cross-thread clobber. Both fail
before the fix (native prints CORRUPT) and pass after.
This commit is contained in:
Graeme Geldenhuys 2026-06-27 11:00:26 +01:00
parent 180fbb62b4
commit a86fb84eea
2 changed files with 176 additions and 21 deletions

View file

@ -7231,7 +7231,7 @@ begin
else if Self.IsLocal(FAE.RecordName) then
Self.EmitLocalRecordBase(FAE.RecordName, '%rcx')
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rcx', [FAE.RecordName]));
Self.EmitLeaqGlobal(FAE.RecordName, '%rcx');
if FAE.FieldInfo.Offset > 0 then
Self.Emit(Format(#9'addq $%d, %%rcx', [FAE.FieldInfo.Offset]));
Self.Emit(#9'movq (%rcx), %rcx'); { string data pointer }
@ -7281,7 +7281,7 @@ begin
else if Self.IsLocal(FAE.RecordName) then
Self.EmitLocalRecordBase(FAE.RecordName, '%rcx')
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rcx', [FAE.RecordName]));
Self.EmitLeaqGlobal(FAE.RecordName, '%rcx');
if FAE.FieldInfo.Offset > 0 then
Self.Emit(Format(#9'addq $%d, %%rcx', [FAE.FieldInfo.Offset]));
if FAE.FieldInfo.TypeDesc.Kind = tyDynArray then
@ -7479,17 +7479,22 @@ begin
if (IsJumboSet(FAE.FieldInfo.TypeDesc) or
(FAE.FieldInfo.TypeDesc.Kind in [tyRecord, tyStaticArray])) then
begin
Self.Emit(Format(#9'leaq %s(%%rip), %%rcx', [FAE.RecordName]));
Self.EmitLeaqGlobal(FAE.RecordName, '%rcx');
if FAE.FieldInfo.Offset = 0 then
Self.Emit(#9'movq %rcx, %rax')
else
Self.Emit(Format(#9'leaq %d(%%rcx), %%rax', [FAE.FieldInfo.Offset]));
end
else if FAE.FieldInfo.Offset = 0 then
else if (FAE.FieldInfo.Offset = 0) and
(not Self.IsThreadVarGlobal(FAE.RecordName)) then
{ Non-threadvar offset-0 fast path: read the field directly off the
PC-relative global. threadvars must not use this — the slot is
TLS-resident, so fall through to EmitLeaqGlobal (which emits the
%fs:0 + @tpoff sequence) and an indirect load. }
Self.EmitLoadVar(FAE.RecordName + '(%rip)', FAE.FieldInfo.TypeDesc)
else
begin
Self.Emit(Format(#9'leaq %s(%%rip), %%rcx', [FAE.RecordName]));
Self.EmitLeaqGlobal(FAE.RecordName, '%rcx');
Self.EmitLoadVar(Format('%d(%%rcx)', [FAE.FieldInfo.Offset]),
FAE.FieldInfo.TypeDesc);
end;
@ -7695,7 +7700,7 @@ begin
if Self.IsLocal(FAE.RecordName) then
Self.EmitLocalRecordBase(FAE.RecordName, '%rcx')
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rcx', [FAE.RecordName]));
Self.EmitLeaqGlobal(FAE.RecordName, '%rcx');
end;
if FAE.FieldInfo.Offset > 0 then
Self.Emit(Format(#9'addq $%d, %%rcx', [FAE.FieldInfo.Offset]));
@ -7772,7 +7777,7 @@ begin
if Self.IsLocal(FAE.RecordName) then
Self.EmitLocalRecordBase(FAE.RecordName, '%rcx')
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rcx', [FAE.RecordName]));
Self.EmitLeaqGlobal(FAE.RecordName, '%rcx');
end;
if (FAE.FieldInfo <> nil) and (FAE.FieldInfo.Offset > 0) then
Self.Emit(Format(#9'addq $%d, %%rcx', [FAE.FieldInfo.Offset]));
@ -8273,7 +8278,7 @@ begin
if Self.IsLocal(ACall.ObjectName) then
Self.Emit(Format(#9'leaq %s, %%r10', [Self.VarOperand(ACall.ObjectName)]))
else
Self.Emit(Format(#9'leaq %s(%%rip), %%r10', [ACall.ObjectName]));
Self.EmitLeaqGlobal(ACall.ObjectName, '%r10');
end
else if ACall.IsVarParam then
begin
@ -8489,7 +8494,7 @@ begin
else if Self.IsLocal(FAE.RecordName) then
Self.EmitLocalRecordBase(FAE.RecordName, '%rax')
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rax', [FAE.RecordName]));
Self.EmitLeaqGlobal(FAE.RecordName, '%rax');
if FAE.FieldInfo.Offset > 0 then
Self.Emit(Format(#9'addq $%d, %%rax', [FAE.FieldInfo.Offset]));
end
@ -9298,7 +9303,7 @@ begin
else if Self.IsLocal(ACall.ObjectName) then
Self.Emit(Format(#9'leaq %s, %%r10', [Self.VarOperand(ACall.ObjectName)]))
else
Self.Emit(Format(#9'leaq %s(%%rip), %%r10', [ACall.ObjectName]));
Self.EmitLeaqGlobal(ACall.ObjectName, '%r10');
end
else if ACall.IsVarParam then
begin
@ -9360,7 +9365,7 @@ begin
else if Self.IsLocal(ACall.ObjectName) then
Self.Emit(Format(#9'leaq %s, %%rax', [Self.VarOperand(ACall.ObjectName)]))
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rax', [ACall.ObjectName]));
Self.EmitLeaqGlobal(ACall.ObjectName, '%rax');
end
else if ACall.IsVarParam then
begin
@ -12054,7 +12059,7 @@ begin
else if Self.IsLocal(FA.RecordName) then
Self.Emit(Format(#9'leaq %s, %%rdx', [Self.VarOperand(FA.RecordName)]))
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rdx', [FA.RecordName]));
Self.EmitLeaqGlobal(FA.RecordName, '%rdx');
if FA.FieldInfo.Offset > 0 then
Self.Emit(Format(#9'addq $%d, %%rdx', [FA.FieldInfo.Offset]));
if FA.FieldInfo.TypeDesc.Kind = tyDynArray then
@ -12148,7 +12153,7 @@ begin
else if Self.IsLocal(FA.RecordName) then
Self.EmitLocalRecordBase(FA.RecordName, '%rcx')
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rcx', [FA.RecordName]));
Self.EmitLeaqGlobal(FA.RecordName, '%rcx');
Self.EmitInterfaceToFieldSlotsAt(FA.Expr, '%rcx', FA.FieldInfo.Offset,
FA.FieldInfo.TypeDesc);
Exit;
@ -12204,7 +12209,7 @@ begin
else if Self.IsLocal(FA.RecordName) then
Self.EmitLocalRecordBase(FA.RecordName, '%rcx')
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rcx', [FA.RecordName]));
Self.EmitLeaqGlobal(FA.RecordName, '%rcx');
if FA.FieldInfo.Offset > 0 then
Self.Emit(Format(#9'addq $%d, %%rcx', [FA.FieldInfo.Offset]));
{ Store the code pointer at offset 0. }
@ -12276,7 +12281,7 @@ begin
if Self.IsLocal(FA.RecordName) then
Self.Emit(Format(#9'leaq %s, %%rbx', [Self.VarOperand(FA.RecordName)]))
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rbx', [FA.RecordName]));
Self.EmitLeaqGlobal(FA.RecordName, '%rbx');
end;
if FA.FieldInfo.Offset > 0 then
Self.Emit(Format(#9'addq $%d, %%rbx', [FA.FieldInfo.Offset]));
@ -12323,7 +12328,7 @@ begin
if Self.IsLocal(FA.RecordName) then
Self.Emit(Format(#9'leaq %s, %%rbx', [Self.VarOperand(FA.RecordName)]))
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rbx', [FA.RecordName]));
Self.EmitLeaqGlobal(FA.RecordName, '%rbx');
end;
if FA.FieldInfo.Offset > 0 then
Self.Emit(Format(#9'addq $%d, %%rbx', [FA.FieldInfo.Offset]));
@ -12394,7 +12399,7 @@ begin
if Self.IsLocal(FA.RecordName) then
Self.EmitLocalRecordBase(FA.RecordName, '%rcx')
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rcx', [FA.RecordName]));
Self.EmitLeaqGlobal(FA.RecordName, '%rcx');
end;
if FA.FieldInfo.Offset > 0 then
Self.Emit(Format(#9'addq $%d, %%rcx', [FA.FieldInfo.Offset]));
@ -12438,7 +12443,7 @@ begin
else if Self.IsLocal(FA.RecordName) then
Self.EmitLocalRecordBase(FA.RecordName, '%rcx')
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rcx', [FA.RecordName]));
Self.EmitLeaqGlobal(FA.RecordName, '%rcx');
if FA.FieldInfo.Offset > 0 then
Self.Emit(Format(#9'addq $%d, %%rcx', [FA.FieldInfo.Offset]));
Self.Emit(#9'pushq %rbx');
@ -12714,7 +12719,7 @@ begin
if Self.IsLocal(FA.RecordName) then
Self.EmitLocalRecordBase(FA.RecordName, '%rcx')
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rcx', [FA.RecordName]));
Self.EmitLeaqGlobal(FA.RecordName, '%rcx');
if FA.FieldInfo.TypeDesc.IsString() then
begin
if not NativeExprOwnsRef(FA.Expr) then
@ -14661,7 +14666,7 @@ begin
else if Self.IsLocal(AFA.RecordName) then
Self.EmitLocalRecordBase(AFA.RecordName, '%rcx')
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rcx', [AFA.RecordName]));
Self.EmitLeaqGlobal(AFA.RecordName, '%rcx');
if AFA.FieldInfo.Offset > 0 then
Self.Emit(Format(#9'addq $%d, %%rcx', [AFA.FieldInfo.Offset]));
end;
@ -15450,7 +15455,7 @@ begin
if Self.IsLocal(ACall.ObjectName) then
Self.Emit(Format(#9'leaq %s, %%rsi', [Self.VarOperand(ACall.ObjectName)]))
else
Self.Emit(Format(#9'leaq %s(%%rip), %%rsi', [ACall.ObjectName]));
Self.EmitLeaqGlobal(ACall.ObjectName, '%rsi');
end
else if ACall.IsVarParam then
begin

View file

@ -32,6 +32,8 @@ type
procedure TestRun_Thread_InheritedDestroy_CleanExit;
procedure TestRun_ThreadVar_MainThread_ReadWrite;
procedure TestRun_ThreadVar_MixedWithGlobalVar;
procedure TestRun_ThreadVar_RecordField_PerThreadIsolation;
procedure TestRun_ThreadVar_RecordMethod_PerThreadIsolation;
procedure TestRun_PerThreadAllocator_IndependentAllocs;
procedure TestRun_AtomicARC_SharedObject_NoCorruption;
end;
@ -367,6 +369,154 @@ begin
AssertEquals('stdout', '30' + LE, Output)
end;
const
{ A RECORD-typed threadvar mutated and read back per-thread. Each thread
stamps its own id into the record-field threadvar, then spins re-reading it
and asserting it never changes. If the record threadvar is addressed
statically (leaq Name(%rip)) instead of via TLS (%fs:0 + @tpoff), all
threads share one slot and clobber each other -> 'CORRUPT'. Scalar
threadvars already use TLS; the bug was that record/aggregate threadvar
field access bypassed it. }
SrcThreadVarRecordIsolation =
'''
program P;
uses Classes;
type
TRec = record
Tag: Integer;
end;
TStampThread = class(TThread)
MyId: Integer;
protected
procedure Execute; override;
end;
threadvar
TVR: TRec;
var
Corrupt: Integer;
procedure TStampThread.Execute;
var I: Integer;
begin
TVR.Tag := Self.MyId;
for I := 0 to 200000 do
if TVR.Tag <> Self.MyId then
begin
Corrupt := 1;
Exit
end
end;
var
A, B, C: TStampThread;
begin
Corrupt := 0;
A := TStampThread.Create(True);
B := TStampThread.Create(True);
C := TStampThread.Create(True);
A.MyId := 11;
B.MyId := 22;
C.MyId := 33;
A.Start();
B.Start();
C.Start();
A.WaitFor();
B.WaitFor();
C.WaitFor();
if Corrupt = 0 then
WriteLn('ok')
else
WriteLn('CORRUPT')
end.
''';
procedure TE2EThreadingTests.TestRun_ThreadVar_RecordField_PerThreadIsolation;
var
Output: string;
RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit end;
AssertTrue('compile+run', CompileAndRunWithRTL(SrcThreadVarRecordIsolation, Output, RCode));
AssertEquals('exit code', 0, RCode);
AssertEquals('stdout', 'ok' + LE, Output)
end;
const
{ Same per-thread isolation guarantee, but the record threadvar is accessed
through METHOD CALLS (GR.Put / GR.Get) rather than direct field access. The
method-call receiver ladder loads the record base too, and used the same
static `leaq Name(%rip)` for a global receiver — so this exercises the
receiver-load path specifically. }
SrcThreadVarRecordMethod =
'''
program P;
uses Classes;
type
TRec = record
N: Integer;
procedure Put(V: Integer);
function Get: Integer;
end;
TStampThread = class(TThread)
MyId: Integer;
protected
procedure Execute; override;
end;
procedure TRec.Put(V: Integer);
begin
N := V
end;
function TRec.Get: Integer;
begin
Result := N
end;
threadvar
GR: TRec;
var
Corrupt: Integer;
procedure TStampThread.Execute;
var I: Integer;
begin
GR.Put(Self.MyId);
for I := 0 to 200000 do
if GR.Get() <> Self.MyId then
begin
Corrupt := 1;
Exit
end
end;
var
A, B, C: TStampThread;
begin
Corrupt := 0;
A := TStampThread.Create(True);
B := TStampThread.Create(True);
C := TStampThread.Create(True);
A.MyId := 11;
B.MyId := 22;
C.MyId := 33;
A.Start();
B.Start();
C.Start();
A.WaitFor();
B.WaitFor();
C.WaitFor();
if Corrupt = 0 then
WriteLn('ok')
else
WriteLn('CORRUPT')
end.
''';
procedure TE2EThreadingTests.TestRun_ThreadVar_RecordMethod_PerThreadIsolation;
var
Output: string;
RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit end;
AssertTrue('compile+run', CompileAndRunWithRTL(SrcThreadVarRecordMethod, Output, RCode));
AssertEquals('exit code', 0, RCode);
AssertEquals('stdout', 'ok' + LE, Output)
end;
const
SrcPerThreadAlloc =
'''