Conversation
Setting an attribute that is already set raises an access violation, and
clearing one leaves it in the array. Both are reachable from
TSyntaxNode.SetAttribute, which is public.
SetAttribute assigned its entry pointer only on the append path:
if not HasAttribute(Key) then
begin
...
AttributeEntry := @FAttributes[len];
end;
AttributeEntry^.Value := Value; //uninitialised when the key existed
so overwriting dereferenced an uninitialised pointer. It now resolves the
entry whether it exists or not, and an empty value removes the attribute
instead of writing through the entry it has just removed.
RemoveAttribute did not remove. It moved the doomed entry FORWARD over its
successor rather than shifting the tail down onto it, never shrank the array,
and computed its length in a mix of bytes and elements. The key left
FAttributesInUse while a stale entry stayed behind, so a later set appended a
second entry for the same key and lookups found the stale one first. It now
shifts the tail down and shrinks, by assignment rather than Move: an entry
holds a managed string, and moving those raw corrupts their reference counts.
Neither had been hit because RemoveAttribute is private with SetAttribute as
its only caller, and nothing in the parser sets one key twice. Anything that
does - a consumer, or a builder recording a position it refines as it goes -
hits both immediately.
AST.SetAttributeTwice covers write, overwrite, clear and re-set. Without the
fix it fails with EAccessViolation on the overwrite; with only the SetAttribute
half it fails on the re-set, returning the stale value.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two bugs in attribute mutation on
TSyntaxNode, both reachable from the publicSetAttribute:Overwrite: uninitialised pointer
SetAttributeassigned its entry pointer only on the append path:When the key exists the
ifblock is skipped, so the dereference below is on an uninitialised local. There is a second problem on the same line: whenValueis empty it callsRemoveAttributeand then writes through the entry it just removed.It now resolves the entry via the existing
TryGetAttributeEntrywhether it exists or not, and an empty value simply removes the attribute and returns.Removal: wrong direction, no shrink, raw Move of managed strings
Index := (NativeUInt(Entry) - NativeUInt(@FAttributes[0])) + Size; Move(Entry^, Pointer(NativeUInt(Entry)+Size)^, (High(FAttributes) * Size) - Index); Exclude(FAttributesInUse, Key);To delete an element the tail has to shift down onto it; this copies the doomed entry forward over its successor. The array is never shrunk, and
Indexmixes byte offsets with an element count. The net effect is that the key leavesFAttributesInUsewhile a stale entry stays inFAttributes— so a laterSetAttributeappends a second entry for the same key, andTryGetAttributeEntryscanning from index 0 finds the stale one first.Moveis also the wrong tool regardless of the arithmetic:TAttributeEntryisTPair<TAttributeName, string>and moving a managed string raw corrupts its reference count. The replacement shifts by assignment and shrinks withSetLength.Why this has not been hit
RemoveAttributeis private withSetAttributeas its only caller, and nothing in the parser sets the same key twice — so neither path is exercised today. Anything that does hits both immediately: a consumer editing a tree, or a builder recording a position it refines as it goes.I found it writing a builder that updates one attribute per parsed directive; the second update crashed.
Verification
AST.SetAttributeTwicecovers write, overwrite, clear, and re-set. It is a plain node — no parsing involved:EAccessViolation, write to an invalid address, on the overwriteSetAttributehalf"again", got"second"Full suite on Delphi 13 Win32: 42 passed of 43. The one failure is pre-existing and unrelated —
Serialization.BinaryRoundTrip, whereline_seqholds a pointer value that does not survive a round trip. FPC via this repo's own workflow: all green.