diff --git a/itab.go b/itab.go new file mode 100644 index 00000000..dc6de393 --- /dev/null +++ b/itab.go @@ -0,0 +1,169 @@ +package goloader + +import ( + "fmt" + "github.com/pkujhd/goloader/mprotect" + "unsafe" +) + +// Similar to runtime.(*itab).init() but replaces method text pointers to start the offset from the specified base address +func (m *itab) adjustMethods(codeBase uintptr, methodIndices []int) string { + inter := m.inter + typ := m._type + x := typ.uncommon() + + ni := len(inter.mhdr) + nt := int(x.mcount) + xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt] + methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni] +imethods: + for k := 0; k < ni; k++ { + i := &inter.mhdr[k] + itype := inter.typ.typeOff(i.ityp) + name := inter.typ.nameOff(i.name) + iname := name.name() + ipkg := name.pkgPath() + if ipkg == "" { + ipkg = inter.pkgpath.name() + } + for _, j := range methodIndices { + t := &xmhdr[j] + if t.ifn < 0 { + panic("shouldn't be possible") + } + tname := typ.nameOff(t.name) + if typ.typeOff(t.mtyp) == itype && tname.name() == iname { + pkgPath := tname.pkgPath() + if pkgPath == "" { + pkgPath = typ.nameOff(x.pkgpath).name() + } + if tname.isExported() || pkgPath == ipkg { + if m != nil { + ifn := unsafe.Pointer(codeBase + uintptr(t.ifn)) + methods[k] = ifn + } + continue imethods + } + } + } + } + return "" +} + +func patchTypeMethods(t *_type, u, prevU *uncommonType, patchedTypeMethodsIfn, patchedTypeMethodsTfn map[*_type][]int) (err error) { + // It's possible that a baked in type in the main module does not have all its methods reachable + // (i.e. some method offsets will be set to -1 via the linker's reachability analysis) whereas the + // new type will have them them all. + + // In this case, to avoid fatal "unreachable method called. linker bug?" errors, we need to + // manipulate the method offsets to make them not -1, and manually partially adjust the + // firstmodule itabs to rewrite the method addresses to point at the new module text (and remember to clean up afterwards) + + if u != nil && prevU != nil { + methods := u.methods() + prevMethods := prevU.methods() + if len(methods) == len(prevMethods) { + for i := range methods { + if methods[i].tfn == -1 || methods[i].ifn == -1 { + + if prevMethods[i].ifn != -1 { + page := mprotect.GetPage(uintptr(unsafe.Pointer(&methods[i].ifn))) + err = mprotect.MprotectMakeWritable(page) + if err != nil { + return fmt.Errorf("failed to make page writeable while patching type %s: %w", _name(t.nameOff(t.str)), err) + } + methods[i].ifn = prevMethods[i].ifn + err = mprotect.MprotectMakeReadOnly(page) + if err != nil { + return fmt.Errorf("failed to make page read only while patching type %s: %w", _name(t.nameOff(t.str)), err) + } + // Store for later cleanup on Unload() + patchedTypeMethodsIfn[t] = append(patchedTypeMethodsIfn[t], i) + } + + if prevMethods[i].tfn != -1 { + page := mprotect.GetPage(uintptr(unsafe.Pointer(&methods[i].tfn))) + err = mprotect.MprotectMakeWritable(page) + if err != nil { + return fmt.Errorf("failed to make page writeable while patching type %s: %w", _name(t.nameOff(t.str)), err) + } + methods[i].tfn = prevMethods[i].tfn + err = mprotect.MprotectMakeReadOnly(page) + if err != nil { + return fmt.Errorf("failed to make page read only while patching type %s: %w", _name(t.nameOff(t.str)), err) + } + // Store for later cleanup on Unload() + patchedTypeMethodsTfn[t] = append(patchedTypeMethodsTfn[t], i) + } + } + } + } + } + return nil +} + +func (cm *CodeModule) patchTypeMethods() (err error) { + // Adjust the main module's itabs so that any missing methods now point to new module's text instead of "unreachable code". + + firstModule := activeModules()[0] + + for _, itab := range firstModule.itablinks { + methodIndicesIfn, ifnPatched := cm.patchedTypeMethodsIfn[itab._type] + methodIndicesTfn, tfnPatched := cm.patchedTypeMethodsTfn[itab._type] + if ifnPatched || tfnPatched { + page := mprotect.GetPage(uintptr(unsafe.Pointer(&itab.fun[0]))) + err = mprotect.MprotectMakeWritable(page) + if err != nil { + return fmt.Errorf("failed to make page writeable while re-initing itab for type %s: %w", _name(itab._type.nameOff(itab._type.str)), err) + } + if ifnPatched { + itab.adjustMethods(uintptr(cm.codeBase), methodIndicesIfn) + } + if tfnPatched { + itab.adjustMethods(uintptr(cm.codeBase), methodIndicesTfn) + } + + err = mprotect.MprotectMakeReadOnly(page) + if err != nil { + return fmt.Errorf("failed to make page read only while re-initing itab for type %s: %w", _name(itab._type.nameOff(itab._type.str)), err) + } + } + } + return nil +} + +func (cm *CodeModule) revertPatchedTypeMethods() error { + for t, indices := range cm.patchedTypeMethodsIfn { + u := t.uncommon() + methods := u.methods() + for _, i := range indices { + page := mprotect.GetPage(uintptr(unsafe.Pointer(&methods[i].ifn))) + err := mprotect.MprotectMakeWritable(page) + if err != nil { + return fmt.Errorf("failed to make page writeable while patching type %s: %w", _name(t.nameOff(t.str)), err) + } + methods[i].ifn = -1 + err = mprotect.MprotectMakeReadOnly(page) + if err != nil { + return fmt.Errorf("failed to make page read only while patching type %s: %w", _name(t.nameOff(t.str)), err) + } + } + } + for t, indices := range cm.patchedTypeMethodsTfn { + u := t.uncommon() + methods := u.methods() + for _, i := range indices { + page := mprotect.GetPage(uintptr(unsafe.Pointer(&methods[i].tfn))) + err := mprotect.MprotectMakeWritable(page) + if err != nil { + return fmt.Errorf("failed to make page writeable while patching type %s: %w", _name(t.nameOff(t.str)), err) + } + methods[i].tfn = -1 + err = mprotect.MprotectMakeReadOnly(page) + if err != nil { + return fmt.Errorf("failed to make page read only while patching type %s: %w", _name(t.nameOff(t.str)), err) + } + } + } + return nil +} diff --git a/ld.go b/ld.go index 82ce28de..e4ac4b09 100644 --- a/ld.go +++ b/ld.go @@ -59,10 +59,12 @@ type Linker struct { type CodeModule struct { segment - Syms map[string]uintptr - module *moduledata - gcdata []byte - gcbss []byte + Syms map[string]uintptr + module *moduledata + gcdata []byte + gcbss []byte + patchedTypeMethodsIfn map[*_type][]int + patchedTypeMethodsTfn map[*_type][]int } var ( @@ -510,6 +512,8 @@ collect: typehash[t.hash] = append(tlist, t) } + patchedTypeMethodsIfn := make(map[*_type][]int) + patchedTypeMethodsTfn := make(map[*_type][]int) segment := &codeModule.segment byteorder := linker.Arch.ByteOrder for _, symbol := range linker.symMap { @@ -529,6 +533,7 @@ collect: // if this is pointing to a type descriptor at an offset inside this binary, we should deduplicate it against // already known types from other modules to allow fast type assertion using *_type pointer equality t := (*_type)(unsafe.Pointer(addr)) + prevT := (*_type)(unsafe.Pointer(addr)) for _, candidate := range typehash[t.hash] { seen := map[_typePair]struct{}{} if typesEqual(t, candidate, seen) { @@ -538,7 +543,14 @@ collect: } // Only relocate code if the type is a duplicate - if uintptr(unsafe.Pointer(t)) != addr { + if t != prevT { + u := t.uncommon() + prevU := prevT.uncommon() + err := patchTypeMethods(t, u, prevU, patchedTypeMethodsIfn, patchedTypeMethodsTfn) + if err != nil { + return err + } + addr = uintptr(unsafe.Pointer(t)) switch loc.Type { case reloctype.R_PCREL: @@ -577,6 +589,14 @@ collect: } } } + codeModule.patchedTypeMethodsIfn = patchedTypeMethodsIfn + codeModule.patchedTypeMethodsTfn = patchedTypeMethodsTfn + + if err != nil { + return err + } + err = codeModule.patchTypeMethods() + return err } @@ -655,6 +675,10 @@ func Load(linker *Linker, symPtr map[string]uintptr) (codeModule *CodeModule, er } func (cm *CodeModule) Unload() error { + err := cm.revertPatchedTypeMethods() + if err != nil { + return err + } removeitabs(cm.module) runtime.GC() modulesLock.Lock() diff --git a/mprotect/mprotect_unix.go b/mprotect/mprotect_unix.go new file mode 100644 index 00000000..fe5c1166 --- /dev/null +++ b/mprotect/mprotect_unix.go @@ -0,0 +1,25 @@ +//go:build darwin || dragonfly || freebsd || linux || openbsd || solaris || netbsd +// +build darwin dragonfly freebsd linux openbsd solaris netbsd + +package mprotect + +import ( + "syscall" + "unsafe" +) + +func GetPage(p uintptr) []byte { + return (*(*[0xFFFFFF]byte)(unsafe.Pointer(p & ^uintptr(syscall.Getpagesize()-1))))[:syscall.Getpagesize()] +} + +func RawMemoryAccess(b uintptr) []byte { + return (*(*[0xFF]byte)(unsafe.Pointer(b)))[:] +} + +func MprotectMakeWritable(page []byte) error { + return syscall.Mprotect(page, syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC) +} + +func MprotectMakeReadOnly(page []byte) error { + return syscall.Mprotect(page, syscall.PROT_READ|syscall.PROT_EXEC) +} diff --git a/mprotect/mprotect_windows.go b/mprotect/mprotect_windows.go new file mode 100644 index 00000000..4c781d42 --- /dev/null +++ b/mprotect/mprotect_windows.go @@ -0,0 +1,6 @@ +//go:build windows +// +build windows + +package mprotect + +// TODO - see if VirtualProtect works? https://learn.microsoft.com/en-gb/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect diff --git a/type.go b/type.go index 164150e0..0de0b9bf 100644 --- a/type.go +++ b/type.go @@ -66,6 +66,12 @@ func _typeOff(t *_type, off typeOff) *_type //go:linkname _name runtime.name.name func _name(n name) string +//go:linkname _pkgPath runtime.name.pkgPath +func _pkgPath(n name) string + +//go:linkname _isExported runtime.name.isExported +func _isExported(n name) bool + //go:linkname _methods reflect.(*uncommonType).methods func _methods(t *uncommonType) []method @@ -79,6 +85,8 @@ func (t *_type) uncommon() *uncommonType { return _uncommon(t) } func (t *_type) nameOff(off nameOff) name { return _nameOff(t, off) } func (t *_type) typeOff(off typeOff) *_type { return _typeOff(t, off) } func (n name) name() string { return _name(n) } +func (n name) pkgPath() string { return _pkgPath(n) } +func (n name) isExported() bool { return _isExported(n) } func (t *uncommonType) methods() []method { return _methods(t) } func (t *_type) Kind() reflect.Kind { return _Kind(t) } func (t *_type) Elem() *_type { return _Elem(t) }