feat(rtl): atomic ARC refcount operations for thread safety

Replace plain read-modify-write refcount operations with atomic
lock xadd instructions via a new blaise_atomic_x86_64.s assembly
stub.  _AtomicAddInt32 and _AtomicSubInt32 return the previous
value, so exactly one thread sees the transition to zero and
triggers destruction -- eliminating the TOCTOU race in
_ClassRelease and _StringRelease.

Affected operations:
  - _ClassAddRef: atomic increment
  - _ClassRelease: atomic decrement, destroy on old_rc == 1
  - _StringAddRef: atomic increment (immortal check unchanged)
  - _StringRelease: atomic decrement, free on old_rc == 1
This commit is contained in:
Graeme Geldenhuys 2026-06-06 12:44:53 +01:00
parent 51a00a5038
commit e093be0b70
4 changed files with 137 additions and 23 deletions

View file

@ -33,6 +33,7 @@ type
procedure TestRun_ThreadVar_MainThread_ReadWrite;
procedure TestRun_ThreadVar_MixedWithGlobalVar;
procedure TestRun_PerThreadAllocator_IndependentAllocs;
procedure TestRun_AtomicARC_SharedObject_NoCorruption;
end;
implementation
@ -416,6 +417,67 @@ begin
AssertEquals('stdout', '2' + LE, Output)
end;
const
SrcAtomicARC =
'''
program P;
uses Classes;
type
TSharedData = class
Value: Integer;
end;
TArcThread = class(TThread)
Shared: TSharedData;
protected
procedure Execute; override;
end;
procedure TArcThread.Execute;
var
Local: TSharedData;
I: Integer;
begin
for I := 0 to 999 do
begin
Local := Self.Shared;
if Local.Value <> 42 then
begin
WriteLn('CORRUPT');
Exit
end;
Local := nil
end
end;
var
S: TSharedData;
A, B, C: TArcThread;
begin
S := TSharedData.Create;
S.Value := 42;
A := TArcThread.Create(True);
B := TArcThread.Create(True);
C := TArcThread.Create(True);
A.Shared := S;
B.Shared := S;
C.Shared := S;
A.Start;
B.Start;
C.Start;
A.WaitFor;
B.WaitFor;
C.WaitFor;
WriteLn('ok')
end.
''';
procedure TE2EThreadingTests.TestRun_AtomicARC_SharedObject_NoCorruption;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit end;
AssertTrue('compile+run', CompileAndRunWithRTL(SrcAtomicARC, Output, RCode));
AssertEquals('exit code', 0, RCode);
AssertEquals('stdout', 'ok' + LE, Output)
end;
initialization
RegisterTest(TE2EThreadingTests);

View file

@ -27,7 +27,8 @@ COMPILER_BIN = ../compiler/target
BLAISE = $(COMPILER_BIN)/blaise
# Assembly sources — platform-specific, no C compiler needed.
ASM_OBJS = $(OBJ_DIR)/blaise_setjmp_x86_64.o
ASM_OBJS = $(OBJ_DIR)/blaise_setjmp_x86_64.o \
$(OBJ_DIR)/blaise_atomic_x86_64.o
# Pascal RTL units compiled via the Blaise compiler.
PAS_OBJS = $(OBJ_DIR)/blaise_mem.o \

View file

@ -0,0 +1,44 @@
#
# Blaise — An Object Pascal Compiler
# Copyright (c) 2026 Graeme Geldenhuys
# SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
# Licensed under the Apache License v2.0 with Runtime Library Exception.
# See LICENSE file in the project root for full license terms.
#
# Atomic 32-bit integer operations for ARC thread safety (x86_64).
#
# These use the x86 LOCK prefix for full sequential-consistency
# atomics. LOCK XADD atomically adds a value to a memory location
# and returns the previous value in the source register.
#
.text
# function _AtomicAddInt32(Ptr: PInteger; Delta: Integer): Integer;
# %rdi = pointer to 32-bit integer
# %esi = value to add
# Returns the value BEFORE the addition.
.globl _AtomicAddInt32
.type _AtomicAddInt32, @function
_AtomicAddInt32:
movl %esi, %eax
lock xaddl %eax, (%rdi)
ret
.size _AtomicAddInt32, .-_AtomicAddInt32
# function _AtomicSubInt32(Ptr: PInteger; Delta: Integer): Integer;
# %rdi = pointer to 32-bit integer
# %esi = value to subtract
# Returns the value BEFORE the subtraction.
.globl _AtomicSubInt32
.type _AtomicSubInt32, @function
_AtomicSubInt32:
negl %esi
movl %esi, %eax
lock xaddl %eax, (%rdi)
ret
.size _AtomicSubInt32, .-_AtomicSubInt32
.section .note.GNU-stack,"",@progbits

View file

@ -10,11 +10,11 @@
Blaise RTL — ARC management (pure Pascal)
String layout — data-pointer convention (shared with blaise_str.pas):
data_ptr − 12 RefCount (Integer, 4 bytes)
data_ptr − 8 Length (Integer, 4 bytes)
data_ptr − 4 Capacity (Integer, 4 bytes)
data_ptr - 12 RefCount (Integer, 4 bytes)
data_ptr - 8 Length (Integer, 4 bytes)
data_ptr - 4 Capacity (Integer, 4 bytes)
data_ptr + 0 UTF-8 char data + NUL terminator
↑ the DATA POINTER is what the variable slot holds
the DATA POINTER is what the variable slot holds
Class instance layout — same offset convention for ARC header:
+--[4 bytes]--+--[4 bytes]--+--[8 bytes]--+--[user fields...]--+
@ -23,6 +23,12 @@
^--- user pointer (what Pascal code sees)
nil = unassigned. RefCount = -1 = immortal (string literals).
Thread safety: all refcount increments and decrements use atomic
lock xadd instructions (via _AtomicAddInt32/_AtomicSubInt32 from
blaise_atomic_x86_64.s). The atomic decrement returns the previous
value, so exactly one thread sees the transition to zero and performs
destruction -- no TOCTOU race.
}
unit blaise_arc;
@ -73,6 +79,11 @@ function _BlaiseGetMem(Size: Integer): Pointer; external name '_BlaiseGetMem';
procedure _BlaiseFreeMem(Ptr: Pointer); external name '_BlaiseFreeMem';
procedure _WeakZeroSlots(Target: Pointer); external name '_WeakZeroSlots';
function _AtomicAddInt32(Ptr: PInteger; Delta: Integer): Integer;
external name '_AtomicAddInt32';
function _AtomicSubInt32(Ptr: PInteger; Delta: Integer): Integer;
external name '_AtomicSubInt32';
procedure MemCopy(Dst, Src: Pointer; N: Integer);
begin
if N > 0 then
@ -347,15 +358,14 @@ end;
procedure _ClassRelease(UserPtr: Pointer);
var
Base: PChar;
RC: PInteger;
OldRC: Integer;
CleanupSlot: PPointer;
Cleanup: TFieldCleanupProc;
begin
if UserPtr = nil then Exit;
Base := PChar(UserPtr) - CLASS_HDR;
RC := PInteger(Base);
RC^ := RC^ - 1;
if RC^ = 0 then
OldRC := _AtomicSubInt32(PInteger(Base), 1);
if OldRC = 1 then
begin
if GLTEnabled then
LTDelete(UserPtr);
@ -376,31 +386,30 @@ end;
procedure _StringAddRef(Ptr: Pointer);
var
RC: ^Integer;
RC: PInteger;
begin
if Ptr = nil then Exit;
RC := Ptr - HDR_SIZE; { RefCount at data_ptr − 12 }
RC := PInteger(Ptr - HDR_SIZE);
if RC^ = IMMORTAL then Exit;
RC^ := RC^ + 1;
_AtomicAddInt32(RC, 1);
end;
procedure _StringRelease(Ptr: Pointer);
var
Base: Pointer;
RC, LN, CP: ^Integer;
RC: PInteger;
LN, CP: ^Integer;
OldRC: Integer;
begin
if Ptr = nil then Exit;
Base := Ptr - HDR_SIZE; { header base = data_ptr − 12 }
RC := Base;
Base := Ptr - HDR_SIZE;
RC := PInteger(Base);
if RC^ = IMMORTAL then Exit;
{ Sanity-check the header before decrement. Catches double-free,
use-after-free, and write-past-end corruption with a clear message
instead of a wild segfault later. }
LN := Base + 4;
CP := Base + 8;
_StringReleaseCheck(Ptr, RC^, LN^, CP^);
RC^ := RC^ - 1;
if RC^ = 0 then _BlaiseFreeMem(Base);
OldRC := _AtomicSubInt32(RC, 1);
if OldRC = 1 then _BlaiseFreeMem(Base);
end;
function _StringEquals(S1, S2: Pointer): Integer;
@ -639,12 +648,10 @@ end;
procedure _ClassAddRef(UserPtr: Pointer);
var
Hdr: Pointer;
RC: ^Integer;
begin
if UserPtr = nil then Exit;
Hdr := UserPtr - CLASS_HDR;
RC := Hdr;
RC^ := RC^ + 1;
_AtomicAddInt32(PInteger(Hdr), 1);
end;
procedure _ClassFree(UserPtr: Pointer);