You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
garble/line_obfuscator.go

179 lines
4.8 KiB
Go

// Copyright (c) 2020, The Garble Authors.
// See LICENSE for licensing information.
package main
import (
"fmt"
"go/ast"
mathrand "math/rand"
"strings"
"golang.org/x/tools/go/ast/astutil"
)
// PosMin is the smallest correct value for the line number.
// Source: https://go.googlesource.com/go/+/refs/heads/master/src/cmd/compile/internal/syntax/parser_test.go#229
const PosMin = 1
const buildTagPrefix = "// +build"
// Source: https://go.googlesource.com/go/+/refs/heads/master/src/cmd/compile/internal/gc/noder.go#1493
var nameSpecialDirectives = []string{
"//go:linkname",
"//go:cgo_export_static",
"//go:cgo_export_dynamic",
"//go:cgo_import_static",
"//go:cgo_import_dynamic",
}
var specialDirectives = append([]string{
"//go:cgo_ldflag",
"//go:cgo_dynamic_linker",
}, nameSpecialDirectives...)
func isDirective(text string, directives []string) bool {
for _, prefix := range directives {
if strings.HasPrefix(text, prefix) {
return true
}
}
return false
}
func getLocalName(text string) (string, bool) {
if !isDirective(text, nameSpecialDirectives) {
return "", false
}
parts := strings.Fields(text)
if len(parts) < 2 {
return "", false
}
name := strings.TrimSpace(parts[1])
if len(name) == 0 {
return "", false
}
return name, true
}
func prependComment(group *ast.CommentGroup, comment *ast.Comment) *ast.CommentGroup {
if group == nil {
return &ast.CommentGroup{List: []*ast.Comment{comment}}
}
group.List = append([]*ast.Comment{comment}, group.List...)
return group
}
// Remove all comments from CommentGroup except //go: directives.
func clearCommentGroup(group *ast.CommentGroup) *ast.CommentGroup {
if group == nil {
return nil
}
var comments []*ast.Comment
for _, comment := range group.List {
if strings.HasPrefix(comment.Text, "//go:") && !isDirective(comment.Text, specialDirectives) {
comments = append(comments, &ast.Comment{Text: comment.Text})
}
}
if len(comments) == 0 {
return nil
}
return &ast.CommentGroup{List: comments}
}
// Remove all comments from Doc (if any) except //go: directives.
func clearNodeComments(node ast.Node) {
switch n := node.(type) {
case *ast.Field:
n.Doc = clearCommentGroup(n.Doc)
n.Comment = nil
case *ast.ImportSpec:
n.Doc = clearCommentGroup(n.Doc)
n.Comment = nil
case *ast.ValueSpec:
n.Doc = clearCommentGroup(n.Doc)
n.Comment = nil
case *ast.TypeSpec:
n.Doc = clearCommentGroup(n.Doc)
n.Comment = nil
case *ast.GenDecl:
n.Doc = clearCommentGroup(n.Doc)
case *ast.FuncDecl:
n.Doc = clearCommentGroup(n.Doc)
case *ast.File:
n.Doc = clearCommentGroup(n.Doc)
}
}
// processDetachedDire collects all not attached to declarations comments and build tags
// It returns detached comments and local name blacklist
func processDetachedDirectives(commentGroups []*ast.CommentGroup) (detachedComments, localNameBlacklist []string) {
var buildTags []string
var specialComments []string
for _, commentGroup := range commentGroups {
for _, comment := range commentGroup.List {
if strings.HasPrefix(comment.Text, buildTagPrefix) {
buildTags = append(buildTags, comment.Text)
continue
}
if !isDirective(comment.Text, specialDirectives) {
continue
}
specialComments = append(specialComments, comment.Text)
if localName, ok := getLocalName(comment.Text); ok {
localNameBlacklist = append(localNameBlacklist, localName)
}
}
}
detachedComments = append(detachedComments, buildTags...)
detachedComments = append(detachedComments, specialComments...)
detachedComments = append(detachedComments, "")
return detachedComments, localNameBlacklist
}
// transformLineInfo removes the comment except go directives and build tags. Converts comments to the node view.
// It returns comments not attached to declarations and names of declarations which cannot be renamed.
func transformLineInfo(file *ast.File, cgoFile bool) (detachedComments, localNameBlacklist []string, f *ast.File) {
prefix := ""
if cgoFile {
prefix = "_cgo_"
}
// Save build tags and add file name leak protection
detachedComments, localNameBlacklist = processDetachedDirectives(file.Comments)
detachedComments = append(detachedComments, "", "//line "+prefix+":1")
file.Comments = nil
newLines := mathrand.Perm(len(file.Decls))
funcCounter := 0
pre := func(cursor *astutil.Cursor) bool {
node := cursor.Node()
clearNodeComments(node)
// If tiny mode is active information about line numbers is erased in object files
if envGarbleTiny {
return true
}
funcDecl, ok := node.(*ast.FuncDecl)
if !ok {
return true
}
comment := &ast.Comment{Text: fmt.Sprintf("//line %s%c.go:%d", prefix, nameCharset[mathrand.Intn(len(nameCharset))], PosMin+newLines[funcCounter])}
funcDecl.Doc = prependComment(funcDecl.Doc, comment)
funcCounter++
return true
}
return detachedComments, localNameBlacklist, astutil.Apply(file, pre, nil).(*ast.File)
}