Skip to content

Overwrite an existing attribute instead of writing through an uninitialised pointer - #1

Merged
partouf merged 2 commits into
mainfrom
fix/setattribute-existing-key
Oct 8, 2026
Merged

partouf merged 2 commits into
mainfrom
fix/setattribute-existing-key

Conversation

@partouf

@partouf partouf commented Oct 8, 2026

Copy link
Copy Markdown
Member

Bug. TSyntaxNode.SetAttribute only pointed AttributeEntry at a slot when it added a new key. For a key the node already carried, the value was written through the uninitialised pointer (dcc32 reports W1036 on AttributeEntry). Any directive stored twice reaches it.

Repro.

unit Example; interface
procedure Same(A: Integer); stdcall; stdcall; external 'a.dll' index 93;
implementation end.

This is legal Delphi. It ends in an access violation ("write of address 0000000B"), or in a silent stray write when the stack happens to hold a valid address. stdcall; cdecl; hits the same path.

The Value = '' path was broken as well. It called RemoveAttribute and then wrote through the same uninitialised pointer. RemoveAttribute itself did not remove anything: it moved the entries one slot forward instead of back, bit-copied the managed strings, and never shortened the array. Only the FAttributesInUse bit was cleared, so Attributes (and with it the writers and the binary serializer) still listed the stale entry.

Fix.

  • SetAttribute looks up an existing entry with TryGetAttributeEntry and overwrites its value, and it adds an entry only for a new key. An empty value removes the attribute and returns. The last value stored wins. W1036 is gone.
  • RemoveAttribute shifts the later entries back by assignment and shortens the array by one.

Tests. Node.AttributeOverwrite, Node.AttributeRemove and AST.RepeatedCallingConvention are added to Test/UnitTests. All three fail on main (two access violations and a stale entry) and pass with this change. Serialization.BinaryRoundTrip fails on main too when built with Delphi 10.4 (line_seq values) and is not affected by this change.

Patrick Quist added 2 commits October 8, 2026 17:42
TSyntaxNode.SetAttribute pointed its entry pointer at a slot only when it
added the key. For a key the node already carried the pointer was left
uninitialised and the value was written through it anyway; dcc32 says so
with W1036 on AttributeEntry. Any directive stored twice reaches it:

  procedure x(a: Integer); stdcall; stdcall; external 'a.dll' index 93;

is legal Delphi, stores anCallingConvention twice and ends in an access
violation, or in a silent write to whatever address the stack held.

SetAttribute now looks the existing entry up with TryGetAttributeEntry and
overwrites its value, and adds an entry only for a new key. An empty value
removes the attribute and returns, instead of falling through to the same
uninitialised pointer after RemoveAttribute. The last value stored wins, so
`stdcall; cdecl;` records cdecl.
SetAttribute with an empty value goes through RemoveAttribute, which did not
remove anything. It computed the byte offset of the entry past the one being
removed and then moved the entries one slot further along rather than back,
so the removed entry stayed, the one before the last was overwritten, and the
array kept its length. Only the key's bit in FAttributesInUse was cleared,
which hid the stale entry from HasAttribute but not from Attributes, so the
writers and the binary serializer still saw it. The Move also copied the
string values bit for bit, leaving two entries owning one reference.

RemoveAttribute now shifts the entries after the removed one back by
assignment, which keeps the string reference counts right, and shortens the
array by one.
@partouf
partouf merged commit 5daadc7 into main Oct 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant