@@ -185,7 +185,10 @@ func AnalyzeObfuscation(t *FunctionTopology) ObfuscationProfile {
185185 }
186186
187187 // 4. Indirect / reflective call ratio from the call signatures the topology
188- // already collected. extractCallSignature tags these "dynamic:" / "reflect:".
188+ // already collected. extractCallSignature tags these "dynamic:" / "invoke:";
189+ // reflection invocation emits the dot-form "reflect.Call" (see
190+ // isIndirectCallSig for the verified form set and why "reflect:" alone was
191+ // insufficient).
189192 totalCalls , indirectCalls := 0 , 0
190193 for sig , n := range t .CallSignatures {
191194 totalCalls += n
@@ -398,12 +401,57 @@ func structuralConstOperands(instr ssa.Instruction) map[*ssa.Value]bool {
398401// "go:invoke:T.M", "defer:invoke:T.M") are NOT matched — only the bare
399402// "invoke:" prefix and "go:dynamic:"/"defer:dynamic:" are. Pinned by
400403// TestIsIndirectCallSig; closing the gap should update that test deliberately.
404+ // isIndirectCallSig reports whether a call signature represents indirect or
405+ // reflective dispatch -- the calls whose target is not statically a named
406+ // function and which therefore evade call-target detection.
407+ //
408+ // SIGNATURE FORMS ARE EMPIRICALLY VERIFIED against topology.ExtractTopology on
409+ // real SSA built with ssa.InstantiateGenerics (the mode both ir/builder.go and
410+ // scan.go use). Do not infer forms from the emitter source alone -- the
411+ // optimizing SSA build resolves some calls differently than NaiveForm would.
412+ //
413+ // - "dynamic:" raw function-pointer call. VERIFIED.
414+ // - "invoke:" interface method dispatch. VERIFIED.
415+ // - "go:dynamic:" dynamic call spawned in a goroutine.
416+ // - "defer:dynamic:" dynamic call in a defer.
417+ // - reflect invocation: reflect.Value.Call and friends. VERIFIED to emit the
418+ // DOT-form "reflect.Call" / "reflect.MethodByName" via extractFunctionSig
419+ // (<pkg>.<func>), NOT the colon-form "reflect:Call". The colon-form branch
420+ // in extractCallSignature is UNREACHABLE under InstantiateGenerics (verified
421+ // by exhausting method-value / method-expression / stored-value shapes), so
422+ // matching only "reflect:" silently MISSED all reflection dispatch and
423+ // under-counted IndirectCallRatio to zero for reflection-hidden capabilities
424+ // -- precisely the evasion technique the signal exists to catch. Both forms
425+ // are now matched; the colon-form is retained for loader-independence.
426+ //
427+ // Only the reflect *invocation* entry points count as indirect dispatch.
428+ // reflect.TypeOf / reflect.ValueOf and similar introspection helpers also emit
429+ // "reflect.<Func>" but are ordinary direct calls, not dynamic dispatch, so they
430+ // are matched by name, not by a blanket "reflect." prefix (which would also
431+ // capture a user package named reflect).
401432func isIndirectCallSig (sig string ) bool {
402433 return hasPrefix (sig , "dynamic:" ) ||
403- hasPrefix (sig , "reflect:" ) ||
434+ hasPrefix (sig , "reflect:" ) || // colon-form: unreachable under this loader, kept defensively
404435 hasPrefix (sig , "invoke:" ) ||
405436 hasPrefix (sig , "go:dynamic:" ) ||
406- hasPrefix (sig , "defer:dynamic:" )
437+ hasPrefix (sig , "defer:dynamic:" ) ||
438+ isReflectInvokeSig (sig )
439+ }
440+
441+ // reflectInvokeMethods are the reflect.Value methods that perform DYNAMIC
442+ // INVOCATION of an underlying callable -- the reflection equivalent of an
443+ // indirect call. Introspection methods (Kind, Type, Field, Len, ...) are
444+ // deliberately excluded: they do not dispatch a call. Emitted dot-form is
445+ // "reflect.<Method>".
446+ var reflectInvokeMethods = map [string ]bool {
447+ "reflect.Call" : true ,
448+ "reflect.CallSlice" : true ,
449+ "reflect.MethodByName" : true , // resolves a method dynamically; its result is invoked
450+ "reflect.Method" : true ,
451+ }
452+
453+ func isReflectInvokeSig (sig string ) bool {
454+ return reflectInvokeMethods [sig ]
407455}
408456
409457// flatteningScore estimates control-flow-flattening from the dispatcher's
@@ -548,3 +596,5 @@ func totalStringLen(ss []string) int {
548596func hasPrefix (s , prefix string ) bool {
549597 return len (s ) >= len (prefix ) && s [:len (prefix )] == prefix
550598}
599+
600+
0 commit comments