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 }