// Copyright 2023 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package runtime import ( "runtime/internal/atomic" "unsafe" ) // Pinner represents a set of pinned Go objects. An object can be pinned with // the Pin method and all pinned objects of a Pinner can be unpinned with the // Unpin method. type Pinner struct { *pinner } // Pin a Go object. The object will not be moved or freed by the garbage // collector until the Unpin method has been called. The pointer to a pinned // object can be directly stored in C memory or can be contained in Go memory // passed to C functions. If the pinned object iftself contains pointers to Go // objects, these objects must be pinned separately if they are going to be // accessed from C code. The argument must be a pointer of any type or an // unsafe.Pointer. It must be a pointer to an object allocated by calling new, // by taking the address of a composite literal, or by taking the address of a // local variable. If one of these conditions is not met, Pin will panic. func (p *Pinner) Pin(pointer any) { if p.pinner == nil { p.pinner = new(pinner) SetFinalizer(p.pinner, func(i *pinner) { if i.refs != nil { i.unpin() // only required to make the test idempotent pinnerLeakPanic() } }) } ptr := pinnerGetPtr(&pointer) setPinned(ptr, true) p.refs = append(p.refs, ptr) } // Unpin all pinned objects of the Pinner. func (p *Pinner) Unpin() { p.pinner.unpin() } type pinner struct { refs []unsafe.Pointer } func (p *pinner) unpin() { if p == nil || p.refs == nil { return } for i := range p.refs { setPinned(p.refs[i], false) p.refs[i] = nil } p.refs = nil } func pinnerGetPtr(i *any) unsafe.Pointer { e := efaceOf(i) etyp := e._type if etyp == nil { panic(errorString("runtime.Pinner: argument is nil")) } if kind := etyp.Kind_ & kindMask; kind != kindPtr && kind != kindUnsafePointer { panic(errorString("runtime.Pinner: argument is not a pointer: " + toRType(etyp).string())) } if inUserArenaChunk(uintptr(e.data)) { // Arena-allocated objects are not eligible for pinning. panic(errorString("runtime.Pinner: object was allocated into an arena")) } return e.data } // isPinned checks if a Go pointer is pinned. // nosplit, because it's called from nosplit code in cgocheck. // //go:nosplit func isPinned(ptr unsafe.Pointer) bool { span := spanOfHeap(uintptr(ptr)) if span == nil { // this code is only called for Go pointer, so this must be a // linker-allocated global object. return true } pinnerBits := span.getPinnerBits() // these pinnerBits might get unlinked by a concurrently running sweep, but // that's OK because gcBits don't get cleared until the following GC cycle // (nextMarkBitArenaEpoch) if pinnerBits == nil { return false } objIndex := span.objIndex(uintptr(ptr)) pinState := pinnerBits.ofObject(objIndex) KeepAlive(ptr) // make sure ptr is alive until we are done so the span can't be freed return pinState.isPinned() } // setPinned marks or unmarks a Go pointer as pinned. func setPinned(ptr unsafe.Pointer, pin bool) { span := spanOfHeap(uintptr(ptr)) if span == nil { if isGoPointerWithoutSpan(ptr) { // this is a linker-allocated or zero size object, nothing to do. return } panic(errorString("runtime.Pinner.Pin: argument is not a Go pointer")) } // ensure that the span is swept, b/c sweeping accesses the specials list // w/o locks. mp := acquirem() span.ensureSwept() KeepAlive(ptr) // make sure ptr is still alive after span is swept objIndex := span.objIndex(uintptr(ptr)) lock(&span.speciallock) // guard against concurrent calls of setPinned on same span pinnerBits := span.getPinnerBits() if pinnerBits == nil { pinnerBits = span.newPinnerBits() span.setPinnerBits(pinnerBits) } pinState := pinnerBits.ofObject(objIndex) if pin { if pinState.isPinned() { // multiple pins on same object, set multipin bit pinState.setMultiPinned(true) // and increase the pin counter // TODO(mknyszek): investigate if systemstack is necessary here systemstack(func() { offset := objIndex * span.elemsize span.incPinCounter(offset) }) } else { // set pin bit pinState.setPinned(true) } } else { // unpin if pinState.isPinned() { if pinState.isMultiPinned() { var exists bool // TODO(mknyszek): investigate if systemstack is necessary here systemstack(func() { offset := objIndex * span.elemsize exists = span.decPinCounter(offset) }) if !exists { // counter is 0, clear multipin bit pinState.setMultiPinned(false) } } else { // no multipins recorded. unpin object. pinState.setPinned(false) } } else { // unpinning unpinned object, bail out throw("runtime.Pinner: object already unpinned") } } unlock(&span.speciallock) releasem(mp) return } type pinState struct { bytep *uint8 byteVal uint8 mask uint8 } // nosplit, because it's called by isPinned, which is nosplit // //go:nosplit func (v *pinState) isPinned() bool { return (v.byteVal & v.mask) != 0 } func (v *pinState) isMultiPinned() bool { return (v.byteVal & (v.mask << 1)) != 0 } func (v *pinState) setPinned(val bool) { v.set(val, false) } func (v *pinState) setMultiPinned(val bool) { v.set(val, true) } // set sets the pin bit of the pinState to val. If multipin is true, it // sets/unsets the multipin bit instead. func (v *pinState) set(val bool, multipin bool) { mask := v.mask if multipin { mask <<= 1 } if val { atomic.Or8(v.bytep, mask) } else { atomic.And8(v.bytep, ^mask) } } // pinnerBits is the same type as gcBits but has different methods. type pinnerBits gcBits // ofObject returns the pinState of the n'th object. // nosplit, because it's called by isPinned, which is nosplit // //go:nosplit func (p *pinnerBits) ofObject(n uintptr) pinState { bytep, mask := (*gcBits)(p).bitp(n * 2) byteVal := atomic.Load8(bytep) return pinState{bytep, byteVal, mask} } func (s *mspan) pinnerBitSize() uintptr { return divRoundUp(s.nelems*2, 8) } // newPinnerBits returns a pointer to 8 byte aligned bytes to be used for this // span's pinner bits. newPinneBits is used to mark objects that are pinned. // They are copied when the span is swept. func (s *mspan) newPinnerBits() *pinnerBits { return (*pinnerBits)(newMarkBits(s.nelems * 2)) } // nosplit, because it's called by isPinned, which is nosplit // //go:nosplit func (s *mspan) getPinnerBits() *pinnerBits { return (*pinnerBits)(atomic.Loadp(unsafe.Pointer(&s.pinnerBits))) } func (s *mspan) setPinnerBits(p *pinnerBits) { atomicstorep(unsafe.Pointer(&s.pinnerBits), unsafe.Pointer(p)) } // refreshPinnerBits replaces pinnerBits with a fresh copy in the arenas for the // next GC cycle. If it does not contain any pinned objects, pinnerBits of the // span is set to nil. func (s *mspan) refreshPinnerBits() { p := s.getPinnerBits() if p == nil { return } hasPins := false bytes := alignUp(s.pinnerBitSize(), 8) // Iterate over each 8-byte chunk and check for pins. Note that // newPinnerBits guarantees that pinnerBits will be 8-byte aligned, so we // don't have to worry about edge cases, irrelevant bits will simply be // zero. for _, x := range unsafe.Slice((*uint64)(unsafe.Pointer(&p.x)), bytes/8) { if x != 0 { hasPins = true break } } if hasPins { newPinnerBits := s.newPinnerBits() memmove(unsafe.Pointer(&newPinnerBits.x), unsafe.Pointer(&p.x), bytes) s.setPinnerBits(newPinnerBits) } else { s.setPinnerBits(nil) } } // incPinCounter is only called for multiple pins of the same object and records // the _additional_ pins. func (span *mspan) incPinCounter(offset uintptr) { var rec *specialPinCounter ref, exists := span.specialFindSplicePoint(offset, _KindSpecialPinCounter) if !exists { lock(&mheap_.speciallock) rec = (*specialPinCounter)(mheap_.specialPinCounterAlloc.alloc()) unlock(&mheap_.speciallock) // splice in record, fill in offset. rec.special.offset = uint16(offset) rec.special.kind = _KindSpecialPinCounter rec.special.next = *ref *ref = (*special)(unsafe.Pointer(rec)) spanHasSpecials(span) } else { rec = (*specialPinCounter)(unsafe.Pointer(*ref)) } rec.counter++ } // decPinCounter decreases the counter. If the counter reaches 0, the counter // special is deleted and false is returned. Otherwise true is returned. func (span *mspan) decPinCounter(offset uintptr) bool { ref, exists := span.specialFindSplicePoint(offset, _KindSpecialPinCounter) if !exists { throw("runtime.Pinner: decreased non-existing pin counter") } counter := (*specialPinCounter)(unsafe.Pointer(*ref)) counter.counter-- if counter.counter == 0 { *ref = counter.special.next if span.specials == nil { spanHasNoSpecials(span) } lock(&mheap_.speciallock) mheap_.specialPinCounterAlloc.free(unsafe.Pointer(counter)) unlock(&mheap_.speciallock) return false } return true } // only for tests func pinnerGetPinCounter(addr unsafe.Pointer) *uintptr { _, span, objIndex := findObject(uintptr(addr), 0, 0) offset := objIndex * span.elemsize t, exists := span.specialFindSplicePoint(offset, _KindSpecialPinCounter) if !exists { return nil } counter := (*specialPinCounter)(unsafe.Pointer(*t)) return &counter.counter } // to be able to test that the GC panics when a pinned pointer is leaking, this // panic function is a variable, that can be overwritten by a test. var pinnerLeakPanic = func() { panic(errorString("runtime.Pinner: found leaking pinned pointer; forgot to call Unpin()?")) }