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:
parent
51a00a5038
commit
e093be0b70
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
44
runtime/src/main/asm/blaise_atomic_x86_64.s
Normal file
44
runtime/src/main/asm/blaise_atomic_x86_64.s
Normal 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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue