github.com/eh-steve/goloader@v0.0.0-20240111193454-90ff3cfdae39/itab.go (about)

     1  package goloader
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/eh-steve/goloader/mprotect"
     6  	"runtime"
     7  	"sort"
     8  	"unsafe"
     9  )
    10  
    11  // modules is a store of all loaded CodeModules for their patches to apply to the firstmodule's itab methods.
    12  // An itab in the first module can only point to a single copy of a method for a given interface+type pair,
    13  // even if multiple CodeModules have included copies of the same method.
    14  // To prevent the unloading of an earlier module leaving firstmodule itabs broken and pointing at an invalid address, we retain a
    15  // mapping of all loaded modules, and which itab methods they have patched, so when one is unloaded, we can apply the
    16  // patches from another if required. This assumes that it doesn't matter which CodeModule provides the extra missing
    17  // methods for a given firstmodule's *_type/interface pair (all modules should have their types deduplicated in the same way).
    18  // Since goloader doesn't do any deadcode elimination, a loaded *_type will always include all available methods
    19  
    20  func getOtherPatchedMethodsForType(t *_type, currentModule *CodeModule) (otherModule *CodeModule, ifn map[int]struct{}, tfn map[int]struct{}, mtyp map[int]typeOff) {
    21  	modulesLock.Lock()
    22  	defer modulesLock.Unlock()
    23  	for module := range modules {
    24  		if module != currentModule {
    25  			var tfnPatched, ifnPatched, mtypPatched bool
    26  			tfn, tfnPatched = module.patchedTypeMethodsTfn[t]
    27  			ifn, ifnPatched = module.patchedTypeMethodsIfn[t]
    28  			mtyp, mtypPatched = module.patchedTypeMethodsMtyp[t]
    29  			if tfnPatched || ifnPatched || mtypPatched {
    30  				otherModule = module
    31  				return
    32  			}
    33  		}
    34  	}
    35  	return nil, nil, nil, nil
    36  }
    37  
    38  func unreachableMethod() {
    39  	panic("goloader patched unreachable method called. linker bug?")
    40  }
    41  
    42  // This var gets updated on darwin if the init() in macho_darwin.go gets run successfully
    43  var textIsWriteable = runtime.GOOS != "darwin"
    44  
    45  // Since multiple CodeModules might patch the first module we need to make sure to store all indices which have ever been patched
    46  var firstModuleMissingMethods = map[*_type]map[int]struct{}{}
    47  
    48  var firstModuleTypemapEntries = map[*_type]typeOff{}
    49  var firstModuleTypemapCounter typeOff = -1
    50  
    51  // Similar to runtime.(*itab).init() but replaces method text pointers to start the offset from the specified base address
    52  func (m *itab) adjustMethods(codeBase uintptr, methodIndices map[int]struct{}, writeablePages map[*byte]struct{}) {
    53  	inter := m.inter
    54  	typ := m._type
    55  	x := typ.uncommon()
    56  
    57  	ni := len(inter.mhdr)
    58  	nt := int(x.mcount)
    59  	xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
    60  	methods := (*[1 << 16]unsafe.Pointer)(unsafe.Pointer(&m.fun[0]))[:ni:ni]
    61  imethods:
    62  	for k := 0; k < ni; k++ {
    63  		i := &inter.mhdr[k]
    64  		itype := inter.typ.typeOff(i.ityp)
    65  		name := inter.typ.nameOff(i.name)
    66  		iname := name.name()
    67  		ipkg := name.pkgPath()
    68  		if ipkg == "" {
    69  			ipkg = inter.pkgpath.name()
    70  		}
    71  		for _, j := range sortInts(methodIndices) {
    72  			t := &xmhdr[j]
    73  			tname := typ.nameOff(t.name)
    74  			if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
    75  				pkgPath := tname.pkgPath()
    76  				if pkgPath == "" {
    77  					pkgPath = typ.nameOff(x.pkgpath).name()
    78  				}
    79  				if tname.isExported() || pkgPath == ipkg {
    80  					if m != nil {
    81  						var ifn unsafe.Pointer
    82  						if t.ifn == -1 {
    83  							// -1 is the sentinel value for unreachable code.
    84  							// See cmd/link/internal/ld/data.go:relocsym.
    85  							ifn = unsafe.Pointer(getFunctionPtr(unreachableMethod))
    86  						} else {
    87  							if uintptr(unsafe.Pointer(typ)) >= firstmoduledata.types && uintptr(unsafe.Pointer(typ)) < firstmoduledata.etypes {
    88  								ifn = unsafe.Pointer(firstmoduledata.text + uintptr(t.ifn))
    89  							} else {
    90  								for md := &firstmoduledata; md != nil; md = md.next {
    91  									if uintptr(unsafe.Pointer(typ)) >= md.types && uintptr(unsafe.Pointer(typ)) < md.etypes {
    92  										ifn = unsafe.Pointer(md.text + uintptr(t.ifn))
    93  									}
    94  								}
    95  							}
    96  						}
    97  						page := mprotect.GetPage(uintptr(unsafe.Pointer(&methods[k])))
    98  						if _, ok := writeablePages[&page[0]]; !ok {
    99  							err := mprotect.MprotectMakeWritable(page)
   100  							if err != nil {
   101  								panic(err)
   102  							}
   103  							writeablePages[&page[0]] = struct{}{}
   104  						}
   105  						methods[k] = ifn
   106  					}
   107  					continue imethods
   108  				}
   109  			}
   110  		}
   111  	}
   112  }
   113  
   114  func (cm *CodeModule) patchTypeMethodOffsets(t *_type, u, prevU *uncommonType, patchedTypeMethodsIfn, patchedTypeMethodsTfn map[*_type]map[int]struct{}, patchedTypeMethodsMtyp map[*_type]map[int]typeOff) (err error) {
   115  	// It's possible that a baked in type in the main module does not have all its methods reachable
   116  	// (i.e. some method offsets will be set to -1 via the linker's reachability analysis) whereas the
   117  	// new type will have them them all.
   118  
   119  	// In this case, to avoid fatal "unreachable method called. linker bug?" errors, we need to
   120  	// manipulate the method text and type offsets to make them not -1, and manually partially adjust the
   121  	// firstmodule itabs to rewrite the method addresses to point at the new module text (and remember to clean up afterwards)
   122  
   123  	if u != nil && prevU != nil && u != prevU && textIsWriteable {
   124  		methods := u.methods()
   125  		prevMethods := prevU.methods()
   126  		if len(methods) == len(prevMethods) {
   127  			for i := range methods {
   128  				missingIndices, found := firstModuleMissingMethods[t]
   129  				if !found {
   130  					missingIndices = map[int]struct{}{}
   131  					firstModuleMissingMethods[t] = missingIndices
   132  				}
   133  				_, markedMissing := missingIndices[i]
   134  
   135  				if methods[i].tfn == -1 || methods[i].ifn == -1 || methods[i].mtyp == -1 || markedMissing {
   136  					missingIndices[i] = struct{}{}
   137  
   138  					if prevMethods[i].ifn != -1 {
   139  						if _, ok := patchedTypeMethodsIfn[t]; !ok {
   140  							patchedTypeMethodsIfn[t] = map[int]struct{}{}
   141  						}
   142  						page := mprotect.GetPage(uintptr(unsafe.Pointer(&methods[i].ifn)))
   143  						err = mprotect.MprotectMakeWritable(page)
   144  						if err != nil {
   145  							return fmt.Errorf("failed to make page writeable while patching type %s %p: %w", _name(t.nameOff(t.str)), unsafe.Pointer(&methods[i].ifn), err)
   146  						}
   147  						// The JIT type's ifn would have been offset with respect to the new type's module's text base.
   148  						// Since we're manipulating the firstmodule's type's methods, we need to recompute the offset with respect to the firstmodule's text base
   149  						methodEntry := cm.module.text + uintptr(prevMethods[i].ifn)
   150  						methods[i].ifn = textOff(methodEntry - firstmoduledata.text)
   151  						err = mprotect.MprotectMakeReadOnly(page)
   152  						if err != nil {
   153  							return fmt.Errorf("failed to make page read only while patching type %s: %w", _name(t.nameOff(t.str)), err)
   154  						}
   155  						// Store for later cleanup on Unload()
   156  						patchedTypeMethodsIfn[t][i] = struct{}{}
   157  					}
   158  
   159  					if prevMethods[i].tfn != -1 {
   160  						if _, ok := patchedTypeMethodsTfn[t]; !ok {
   161  							patchedTypeMethodsTfn[t] = map[int]struct{}{}
   162  						}
   163  						page := mprotect.GetPage(uintptr(unsafe.Pointer(&methods[i].tfn)))
   164  						err = mprotect.MprotectMakeWritable(page)
   165  						if err != nil {
   166  							return fmt.Errorf("failed to make page writeable while patching type %s: %w", _name(t.nameOff(t.str)), err)
   167  						}
   168  
   169  						// The JIT type's tfn would have been offset with respect to the new type's module's text base.
   170  						// Since we're manipulating the firstmodule's type's methods, we need to recompute the offset with respect to the firstmodule's text base
   171  						methodEntry := cm.module.text + uintptr(prevMethods[i].tfn)
   172  						methods[i].tfn = textOff(methodEntry - firstmoduledata.text)
   173  						err = mprotect.MprotectMakeReadOnly(page)
   174  						if err != nil {
   175  							return fmt.Errorf("failed to make page read only while patching type %s: %w", _name(t.nameOff(t.str)), err)
   176  						}
   177  						// Store for later cleanup on Unload()
   178  						patchedTypeMethodsTfn[t][i] = struct{}{}
   179  					}
   180  
   181  					if prevMethods[i].mtyp != -1 && methods[i].mtyp < 0 {
   182  						if _, ok := patchedTypeMethodsMtyp[t]; !ok {
   183  							patchedTypeMethodsMtyp[t] = map[int]typeOff{}
   184  						}
   185  						page := mprotect.GetPage(uintptr(unsafe.Pointer(&methods[i].mtyp)))
   186  						err = mprotect.MprotectMakeWritable(page)
   187  						if err != nil {
   188  							return fmt.Errorf("failed to make page writeable while patching type %s: %w", _name(t.nameOff(t.str)), err)
   189  						}
   190  
   191  						// The JIT type's mtyp would have been offset with respect to the new type's module's data base.
   192  						// Since the runtime assumes that types and their methods' types are defined in the same module, but we have a situation where the type
   193  						// is in the firstmodule, but method type is in a JIT module, we have to hack around runtime.resolveTypeOff
   194  						// by adding an entry under a negative offset (< -1) to the firstmodule's typemap
   195  						methodType := (*_type)(unsafe.Pointer(cm.module.types + uintptr(prevMethods[i].mtyp)))
   196  						firstModuleTypemapCounter--
   197  						if firstmoduledata.typemap == nil {
   198  							firstmoduledata.typemap = make(map[typeOff]*_type, len(firstmoduledata.typelinks)+1)
   199  							for _, tl := range firstmoduledata.typelinks {
   200  								firstmoduledata.typemap[typeOff(tl)] = (*_type)(unsafe.Pointer(firstmoduledata.types + uintptr(tl)))
   201  							}
   202  							*pinnedTypemapsTyped = append(*pinnedTypemapsTyped, firstmoduledata.typemap)
   203  						}
   204  						firstmoduledata.typemap[firstModuleTypemapCounter] = methodType
   205  						firstModuleTypemapEntries[methodType] = firstModuleTypemapCounter
   206  
   207  						cm.module.typemap[firstModuleTypemapCounter] = methodType // In case we need to resolve this method type with respect to the JIT type (unlikely since it should have been deduped with the firstmodule type?)
   208  						methods[i].mtyp = firstModuleTypemapCounter
   209  
   210  						err = mprotect.MprotectMakeReadOnly(page)
   211  						if err != nil {
   212  							return fmt.Errorf("failed to make page read only while patching type %s: %w", _name(t.nameOff(t.str)), err)
   213  						}
   214  						// Store for later cleanup on Unload()
   215  						patchedTypeMethodsMtyp[t][i] = firstModuleTypemapCounter
   216  						markedMissing = true
   217  					}
   218  
   219  					if markedMissing {
   220  						if _, ok := patchedTypeMethodsIfn[t]; !ok {
   221  							patchedTypeMethodsIfn[t] = map[int]struct{}{}
   222  						}
   223  						if _, ok := patchedTypeMethodsTfn[t]; !ok {
   224  							patchedTypeMethodsTfn[t] = map[int]struct{}{}
   225  						}
   226  						patchedTypeMethodsIfn[t][i] = struct{}{}
   227  						patchedTypeMethodsTfn[t][i] = struct{}{}
   228  					}
   229  
   230  				}
   231  			}
   232  		}
   233  	}
   234  	return nil
   235  }
   236  
   237  func firstModuleItabsByType() map[*_type][]*itab {
   238  	firstModule := activeModules()[0]
   239  	result := map[*_type][]*itab{}
   240  	for _, itab := range firstModule.itablinks {
   241  		result[itab._type] = append(result[itab._type], itab)
   242  	}
   243  	return result
   244  }
   245  
   246  var sortedInts []int
   247  
   248  func sortInts(m map[int]struct{}) []int {
   249  	sortedInts = sortedInts[:0]
   250  	for i := range m {
   251  		sortedInts = append(sortedInts, i)
   252  	}
   253  	sort.Ints(sortedInts)
   254  	return sortedInts
   255  }
   256  
   257  func patchTypeMethodTextPtrs(codeBase uintptr, patchedTypeMethodsIfn, patchedTypeMethodsTfn map[*_type]map[int]struct{}) (err error) {
   258  	// Adjust the main module's itabs so that any missing methods now point to new module's text instead of "unreachable code".
   259  
   260  	firstModule := activeModules()[0]
   261  
   262  	var writeablePages = map[*byte]struct{}{}
   263  	for _, itab := range firstModule.itablinks {
   264  		methodIndicesIfn, ifnPatched := patchedTypeMethodsIfn[itab._type]
   265  		methodIndicesTfn, tfnPatched := patchedTypeMethodsTfn[itab._type]
   266  		if ifnPatched || tfnPatched {
   267  			page := mprotect.GetPage(uintptr(unsafe.Pointer(&itab.fun[0])))
   268  			if _, ok := writeablePages[&page[0]]; !ok {
   269  				err = mprotect.MprotectMakeWritable(page)
   270  				if err != nil {
   271  					return fmt.Errorf("failed to make page writeable while re-initing itab for type %s %p: %w", _name(itab._type.nameOff(itab._type.str)), unsafe.Pointer(&itab.fun[0]), err)
   272  				}
   273  				writeablePages[&page[0]] = struct{}{}
   274  			}
   275  			if ifnPatched {
   276  				itab.adjustMethods(codeBase, methodIndicesIfn, writeablePages)
   277  			}
   278  			if tfnPatched {
   279  				itab.adjustMethods(codeBase, methodIndicesTfn, writeablePages)
   280  			}
   281  
   282  		}
   283  	}
   284  
   285  	for pageStart := range writeablePages {
   286  		err = mprotect.MprotectMakeReadOnly(mprotect.GetPage(uintptr(unsafe.Pointer(pageStart))))
   287  		if err != nil {
   288  			return fmt.Errorf("failed to make page %p read only while re-initing itab : %w", pageStart, err)
   289  		}
   290  	}
   291  	return nil
   292  }
   293  
   294  func (cm *CodeModule) revertPatchedTypeMethods() error {
   295  	firstModuleItabs := firstModuleItabsByType()
   296  
   297  	var writeablePages = map[*byte]struct{}{}
   298  	for t, indices := range cm.patchedTypeMethodsIfn {
   299  		u := t.uncommon()
   300  		methods := u.methods()
   301  		// Check if we have any other modules available which provide the same methods
   302  		otherModule, ifnPatchedOther, tfnPatchedOther, _ := getOtherPatchedMethodsForType(t, cm)
   303  		if otherModule != nil {
   304  			for _, itab := range firstModuleItabs[t] {
   305  				itab.adjustMethods(uintptr(otherModule.codeBase), ifnPatchedOther, writeablePages)
   306  				itab.adjustMethods(uintptr(otherModule.codeBase), tfnPatchedOther, writeablePages)
   307  			}
   308  		} else {
   309  			// Reset patched method offsets back to -1
   310  			for _, i := range sortInts(indices) {
   311  				page := mprotect.GetPage(uintptr(unsafe.Pointer(&methods[i].ifn)))
   312  				if _, ok := writeablePages[&page[0]]; !ok {
   313  					err := mprotect.MprotectMakeWritable(page)
   314  					if err != nil {
   315  						return fmt.Errorf("failed to make page writeable while patching type %s %p: %w", _name(t.nameOff(t.str)), unsafe.Pointer(&methods[i].ifn), err)
   316  					}
   317  					writeablePages[&page[0]] = struct{}{}
   318  				}
   319  				methods[i].ifn = -1
   320  			}
   321  			for _, itab := range firstModuleItabs[t] {
   322  				// No other module found, all method offsets should be -1, so codeBase is irrelevant
   323  				itab.adjustMethods(0, indices, writeablePages)
   324  			}
   325  
   326  		}
   327  	}
   328  	for t, indices := range cm.patchedTypeMethodsTfn {
   329  		u := t.uncommon()
   330  		methods := u.methods()
   331  		// Check if we have any other modules available which provide the same methods
   332  		otherModule, ifnPatchedOther, tfnPatchedOther, _ := getOtherPatchedMethodsForType(t, cm)
   333  		if otherModule != nil {
   334  			for _, itab := range firstModuleItabs[t] {
   335  				itab.adjustMethods(uintptr(otherModule.codeBase), ifnPatchedOther, writeablePages)
   336  				itab.adjustMethods(uintptr(otherModule.codeBase), tfnPatchedOther, writeablePages)
   337  			}
   338  		} else {
   339  			// Reset patched method offsets back to -1
   340  			for _, i := range sortInts(indices) {
   341  				page := mprotect.GetPage(uintptr(unsafe.Pointer(&methods[i].tfn)))
   342  				if _, ok := writeablePages[&page[0]]; !ok {
   343  					err := mprotect.MprotectMakeWritable(page)
   344  					if err != nil {
   345  						return fmt.Errorf("failed to make page writeable while patching type %s %p: %w", _name(t.nameOff(t.str)), unsafe.Pointer(&methods[i].tfn), err)
   346  					}
   347  					writeablePages[&page[0]] = struct{}{}
   348  				}
   349  				methods[i].tfn = -1
   350  			}
   351  			for _, itab := range firstModuleItabs[t] {
   352  				// No other module found, all method offsets should be -1, so codeBase is irrelevant
   353  				itab.adjustMethods(0, indices, writeablePages)
   354  			}
   355  		}
   356  	}
   357  
   358  	for t, indices := range cm.patchedTypeMethodsMtyp {
   359  		u := t.uncommon()
   360  		methods := u.methods()
   361  		// Check if we have any other modules available which provide the same methods
   362  		otherModule, _, _, mtypPatchedOther := getOtherPatchedMethodsForType(t, cm)
   363  		if otherModule != nil {
   364  			for i := range indices {
   365  				if otherTypeOff, ok := mtypPatchedOther[i]; ok {
   366  					page := mprotect.GetPage(uintptr(unsafe.Pointer(&methods[i].mtyp)))
   367  					if _, ok := writeablePages[&page[0]]; !ok {
   368  						err := mprotect.MprotectMakeWritable(page)
   369  						if err != nil {
   370  							return fmt.Errorf("failed to make page writeable while patching type %s %p: %w", _name(t.nameOff(t.str)), unsafe.Pointer(&methods[i].tfn), err)
   371  						}
   372  						writeablePages[&page[0]] = struct{}{}
   373  					}
   374  					delete(firstmoduledata.typemap, methods[i].mtyp)
   375  					methods[i].mtyp = otherTypeOff
   376  				}
   377  			}
   378  		} else {
   379  			// Reset patched method type offsets back to -1, and delete firstmoduledata.typemap entries
   380  			for i := range indices {
   381  				page := mprotect.GetPage(uintptr(unsafe.Pointer(&methods[i].mtyp)))
   382  				if _, ok := writeablePages[&page[0]]; !ok {
   383  					err := mprotect.MprotectMakeWritable(page)
   384  					if err != nil {
   385  						return fmt.Errorf("failed to make page writeable while patching type %s %p: %w", _name(t.nameOff(t.str)), unsafe.Pointer(&methods[i].tfn), err)
   386  					}
   387  					writeablePages[&page[0]] = struct{}{}
   388  				}
   389  				if methods[i].mtyp < -1 {
   390  					delete(firstmoduledata.typemap, methods[i].mtyp)
   391  					methods[i].mtyp = -1
   392  				}
   393  			}
   394  		}
   395  	}
   396  
   397  	for pageStart := range writeablePages {
   398  		err := mprotect.MprotectMakeReadOnly(mprotect.GetPage(uintptr(unsafe.Pointer(pageStart))))
   399  		if err != nil {
   400  			return fmt.Errorf("failed to make page %p read only while re-initing itab: %w", pageStart, err)
   401  		}
   402  	}
   403  	return nil
   404  }