Skip to content

fix ssa2ast bugs - #1080

Merged
mvdan merged 10 commits into
burrowers:masterfrom
pagran:ssa2ast-fixes08
Aug 25, 2026
Merged

fix ssa2ast bugs#1080
mvdan merged 10 commits into
burrowers:masterfrom
pagran:ssa2ast-fixes08

Conversation

@pagran

@pagran pagran commented Aug 25, 2026

Copy link
Copy Markdown
Member

Fix a batch of bugs in the ssa2ast converter used by the control flow obfuscation, so it correctly handles more valid Go constructs

p.s. partly found via LLM fuzzing

pagran added 10 commits August 24, 2026 22:59
Convert failed with "instruction multiconvert ... : unsupported" for
generic conversions whose type set spans multiple conversion forms, such
as converting a type parameter to a concrete type:

    func genericConvert[T ~int64 | ~float64](x T) float64 {
        return float64(x)
    }

MultiConvert is a representation-changing conversion, so emit the same
(T)(x) cast as Convert.
The recvOk tuple slot of a select receive was declared but never assigned,
so "case v, ok := <-ch" always observed ok == false (the zero value) even
when a value was received from an open channel:

    select {
    case v, ok := <-ch:
        fmt.Println(v, ok) // ok was always false

Emit the receive with comma-ok (v, ok = <-ch) when the ok slot has uses.
Ranging over a named map or string type panicked:

    panic: interface conversion: types.Type is *types.Named, not *types.Map

    type namedMap map[string]int
    type namedString string

    for k := range namedMap{...} {...}       // panicked
    for _, r := range namedString("x") {...} // panicked

isStringType now checks the underlying type, and the map iterator polyfill
is built from the underlying *types.Map instead of asserting directly.
The string range polyfill iterated []rune(x) and reported the rune index
as the range key, but a string range yields byte offsets. For non-ASCII
strings the two diverge:

    for i, r := range "héllo" {
        fmt.Println(i, r) // got 0,1,2,3,4; want 0,1,3,4,5
    }

Track the byte offset separately and advance it by len(string(r)) per rune
while the rune slice supplies the values.
Converting a bound method value failed with:

    make closure for non anon func "Return1$bound": unsupported

x/tools lowers `f := strct.Return1` (a MethodVal) to a MakeClosure over a
synthetic `$bound` wrapper whose only free variable is the receiver, so
the converter must not treat it as an anonymous function.

Reconstruct the method value as `recv.method`; for instantiated methods
the wrapper name carries type args (`get[int]$bound`) which are stripped
since the method value infers them from the receiver type.
A named type declared inside a nested block (if/for/switch) produced
"undefined: local" in the converted output. The previous check only
inlined function-scoped types, using a reflect hack on the unexported
Scope.isFunc field; a block scope is not a function scope, so such types
were left as dangling identifier references.

    func f() {
        if true {
            type local struct{ X int }
            _ = local{1} // converted to: undefined: local
        }
    }

Inline any named type whose declaration scope is not its package scope,
and drop the reflect dependency.
A composite literal of a struct with a blank field ("_ int") produced
"undefined: x._" in the converted output, because the field address was
reconstructed as &x._ — but blank fields cannot be referenced in Go.

    type S struct {
        _ int
        X int
    }
    s := S{5, 7} // converted to: &s._ (invalid)

Emit new(T) for the blank field's address instead; the subsequent store
becomes a no-op that still evaluates its value expression, preserving any
side effects.
The byte-index polyfill advanced the offset by len(string(r)), the UTF-8
width of the decoded rune. For an invalid byte the range yields U+FFFD,
whose own encoding is 3 bytes wide, but the range advances only 1 byte:

    for i, r := range "\xff\x41" {
        fmt.Println(i, r) // got 0,3; want 0,1
    }

Build the rune slice and its byte offsets together with a native range
over the string (which decodes invalid sequences correctly) instead of
deriving offsets from []rune(x) and each rune's own width.
Inlining a generic type declared in a block scope used the declaration's
underlying type, which still references the generic type parameter, so the
converted output referenced an undefined T:

    func f() {
        type pair[T any] struct{ a, b T }
        p := pair[int]{1, 2} // converted to: struct{ a T; b T } (undefined: T)
    }

Inline the instantiated type's underlying type (typ.Underlying()) instead
of the declaration's (obj.Type().Underlying()), so the type arguments are
substituted before conversion.
Field access on a struct referenced through a type alias failed:

    type Local = struct{ X int }
    x := Local{1}
    fmt.Println(x.X) // field 0 not found in test/main.Local

getFieldName unwrapped pointers and named types but not aliases, so a
*types.Alias never reached the struct case. Resolve aliases with
types.Unalias before unwrapping the named type.
@mvdan
mvdan merged commit 3ebce21 into burrowers:master Aug 25, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants