fix: mark tags fmt cannot format safely instead of crashing (#197)#209
Open
linkdata wants to merge 1 commit into
Open
fix: mark tags fmt cannot format safely instead of crashing (#197)#209linkdata wants to merge 1 commit into
linkdata wants to merge 1 commit into
Conversation
linkdata
force-pushed
the
fix/tagstring-cyclic-stack-overflow-197
branch
from
July 21, 2026 22:49
c7df1ba to
63c13e5
Compare
TagString formatted tags with fmt's %#v and Message.String formatted Dest with %v. Neither verb detects reference cycles, so a self-referential tag list — a []any that contains itself, or the same cycle reached through a value struct or a map — recurses until the runtime aborts the process with "fatal error: stack overflow", an unrecoverable crash from valid, supported input. A compact value that expands into an enormous string (an exponentially shared graph, or many references to one large value) makes fmt buffer gigabytes for the same reason. Add a small pre-check (formatSafe) that walks a value the way fmt does and decides whether it is safe to hand over: fmt.Formatter/GoStringer/Stringer values are leaves, a pointer is descended only at the top level, and the recursion depth and approximate output size are bounded. A reference cycle is just an unbounded-depth walk, so the depth bound catches it with no active-node bookkeeping; the size bound accounts for %#v type strings (which reflect.StructOf can make huge) and stops exponential or oversized output. A value that passes is handed to fmt unchanged, so ordinary acyclic tags format exactly as before, custom formatters included; a value that fails renders as the marker "cyclic-tag". A top-level reflect.Value is unwrapped first, as fmt does. TagString and Message.String share this via the new tag.FormatValue. Fixes #197
linkdata
force-pushed
the
fix/tagstring-cyclic-stack-overflow-197
branch
from
July 21, 2026 23:16
63c13e5 to
601f3e9
Compare
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.
Fixes #197.
Problem
tag.TagStringformatted tags withfmt's%#vandwire.Message.StringformattedDestwith%v. Neither verb detects reference cycles, so a self-referential tag list recurses until the runtime aborts the process withfatal error: stack overflow— unrecoverable, from valid input. Confirmed empirically for a cyclic[]any, a value-struct-through-slice cycle, and a cyclic map (pointer cycles are safe —fmtprints an address). A compact value that expands exponentially makesfmtbuffer 1.3 GB.Approach
Detect, then delegate. A small pre-check (
formatSafe) decides before callingfmtwhether a value is safe to hand over — necessary becausefmtbuilds its whole output in an internal buffer before writing, so neither a boundedio.Writernorrecovercan contain the overflow or the memory blow-up (both verified). The check walks the value the wayfmtdoes:fmt.Formatter/fmt.GoStringer/fmt.Stringervalues are leaves (fmt calls the method, doesn't recurse structure);s[:1]are never misread as cycles);%#vtype string (whichreflect.StructOfcan make enormous), catching exponential/huge blow-ups;reflect.Valueis unwrapped first, matching fmt.A value that passes is handed to
fmtunchanged — every ordinary acyclic tag formats exactly as before, custom formatters (including redaction) included, andMessage.Stringkeeps its precise%voutput. A value that fails renders as the markercyclic-taginstead of crashing.TagStringandMessage.Stringshare the logic through the newtag.FormatValue(v, goSyntax)— the fmt-path analogue of the cycle detectionTagExpand/expandalready use for tag expansion. As withTagExpand, formatting methods reachable from the value must terminate and not mutate the graph (documented precondition).Tests & benchmarks
Covers the issue reproduction and the cyclic struct/map/array/unexported and
reflect.Valuecases (all →cyclic-tag, no crash), the exponential-graph / huge-shared-string / huge-reflect.StructOf-type-name blow-ups, the depth cap, pointer cycles and large/aliased acyclic slices (not flagged — still byte-identical tofmt), and customFormatter/GoStringer/Stringer/errorhandling (including a >1 MiB string-backed redactor). A benchmark guards the deep-chain path that a naive active-node scan made quadratic (~2.9 s → 1.1 µs / 0 allocs). 100% statement coverage of the new code;go vet,gofumpt,staticcheck,golangci-lint(0 issues) andgo test -race ./...all pass.