Begin thread code isolation
This commit is contained in:
parent
d41bbbf5c3
commit
64e01bfed1
@ -3,7 +3,6 @@ package proctl_test
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/derekparker/delve/helper"
|
||||
@ -12,7 +11,7 @@ import (
|
||||
|
||||
func dataAtAddr(pid int, addr uint64) ([]byte, error) {
|
||||
data := make([]byte, 1)
|
||||
_, err := syscall.PtracePeekData(pid, uintptr(addr), data)
|
||||
_, err := proctl.ReadMemory(pid, uintptr(addr), data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
315
proctl/threads.go
Normal file
315
proctl/threads.go
Normal file
@ -0,0 +1,315 @@
|
||||
package proctl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"syscall"
|
||||
|
||||
"github.com/derekparker/delve/dwarf/frame"
|
||||
)
|
||||
|
||||
// ThreadContext represents a single thread of execution in the
|
||||
// traced program.
|
||||
type ThreadContext struct {
|
||||
Id int
|
||||
Process *DebuggedProcess
|
||||
Status *syscall.WaitStatus
|
||||
Regs *syscall.PtraceRegs
|
||||
}
|
||||
|
||||
// Obtains register values from the debugged process.
|
||||
func (thread *ThreadContext) Registers() (*syscall.PtraceRegs, error) {
|
||||
err := syscall.PtraceGetRegs(thread.Id, thread.Regs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get registers %s", err)
|
||||
}
|
||||
|
||||
return thread.Regs, nil
|
||||
}
|
||||
|
||||
// Returns the current PC for this thread id.
|
||||
func (thread *ThreadContext) CurrentPC() (uint64, error) {
|
||||
regs, err := thread.Registers()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return regs.PC(), nil
|
||||
}
|
||||
|
||||
// PrintInfo prints out the thread status
|
||||
// including: PC, tid, file, line, and function.
|
||||
func (thread *ThreadContext) PrintInfo() error {
|
||||
pc, err := thread.CurrentPC()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, l, fn := thread.Process.GoSymTable.PCToLine(pc)
|
||||
if fn != nil {
|
||||
fmt.Printf("Thread %d at %#v %s:%d %s\n", thread.Id, pc, f, l, fn.Name)
|
||||
} else {
|
||||
fmt.Printf("Thread %d at %#v\n", thread.Id, pc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sets a software breakpoint at addr, and stores it in the process wide
|
||||
// break point table. Setting a break point must be thread specific due to
|
||||
// ptrace actions needing the thread to be in a signal-delivery-stop in order
|
||||
// to initiate any ptrace command. Otherwise, it really doesn't matter
|
||||
// as we're only dealing with threads.
|
||||
func (thread *ThreadContext) Break(addr uintptr) (*BreakPoint, error) {
|
||||
var (
|
||||
int3 = []byte{0xCC}
|
||||
f, l, fn = thread.Process.GoSymTable.PCToLine(uint64(addr))
|
||||
originalData = make([]byte, 1)
|
||||
)
|
||||
|
||||
if fn == nil {
|
||||
return nil, InvalidAddressError{address: addr}
|
||||
}
|
||||
|
||||
_, err := ReadMemory(thread.Id, addr, originalData)
|
||||
if err != nil {
|
||||
fmt.Println("PEEK ERR")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bytes.Equal(originalData, int3) {
|
||||
return nil, BreakPointExistsError{f, l, addr}
|
||||
}
|
||||
|
||||
_, err = WriteMemory(thread.Id, addr, int3)
|
||||
if err != nil {
|
||||
fmt.Println("POKE ERR")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
breakpoint := &BreakPoint{
|
||||
FunctionName: fn.Name,
|
||||
File: f,
|
||||
Line: l,
|
||||
Addr: uint64(addr),
|
||||
OriginalData: originalData,
|
||||
}
|
||||
|
||||
thread.Process.BreakPoints[uint64(addr)] = breakpoint
|
||||
|
||||
return breakpoint, nil
|
||||
}
|
||||
|
||||
// Clears a software breakpoint, and removes it from the process level
|
||||
// break point table.
|
||||
func (thread *ThreadContext) Clear(pc uint64) (*BreakPoint, error) {
|
||||
bp, ok := thread.Process.BreakPoints[pc]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("No breakpoint currently set for %#v", pc)
|
||||
}
|
||||
|
||||
if _, err := WriteMemory(thread.Id, uintptr(bp.Addr), bp.OriginalData); err != nil {
|
||||
return nil, fmt.Errorf("could not clear breakpoint %s", err)
|
||||
}
|
||||
|
||||
delete(thread.Process.BreakPoints, pc)
|
||||
|
||||
return bp, nil
|
||||
}
|
||||
|
||||
func (thread *ThreadContext) Continue() error {
|
||||
// Check whether we are stopped at a breakpoint, and
|
||||
// if so, single step over it before continuing.
|
||||
regs, err := thread.Registers()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get registers %s", err)
|
||||
}
|
||||
|
||||
if _, ok := thread.Process.BreakPoints[regs.PC()-1]; ok {
|
||||
err := thread.Step()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not step %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return syscall.PtraceCont(thread.Id, 0)
|
||||
}
|
||||
|
||||
// Single steps this thread a single instruction, ensuring that
|
||||
// we correctly handle the likely case that we are at a breakpoint.
|
||||
func (thread *ThreadContext) Step() (err error) {
|
||||
regs, err := thread.Registers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bp, ok := thread.Process.BreakPoints[regs.PC()-1]
|
||||
if ok {
|
||||
// Clear the breakpoint so that we can continue execution.
|
||||
_, err = thread.Clear(bp.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset program counter to our restored instruction.
|
||||
regs.SetPC(bp.Addr)
|
||||
err = syscall.PtraceSetRegs(thread.Id, regs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not set registers %s", err)
|
||||
}
|
||||
|
||||
// Restore breakpoint now that we have passed it.
|
||||
defer func() {
|
||||
_, err = thread.Break(uintptr(bp.Addr))
|
||||
}()
|
||||
}
|
||||
|
||||
err = syscall.PtraceSingleStep(thread.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("step failed: %s", err.Error())
|
||||
}
|
||||
|
||||
_, _, err = wait(thread.Id, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step to next source line. Next will step over functions,
|
||||
// and will follow through to the return address of a function.
|
||||
// Next is implemented on the thread context, however during the
|
||||
// course of this function running, it's very likely that the
|
||||
// goroutine our M is executing will switch to another M, therefore
|
||||
// this function cannot assume all execution will happen on this thread
|
||||
// in the traced process.
|
||||
func (thread *ThreadContext) Next() (err error) {
|
||||
pc, err := thread.CurrentPC()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := thread.Process.BreakPoints[pc-1]; ok {
|
||||
pc-- // Decrement PC to account for BreakPoint
|
||||
}
|
||||
|
||||
_, l, _ := thread.Process.GoSymTable.PCToLine(pc)
|
||||
fde, err := thread.Process.FrameEntries.FDEForPC(pc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
step := func() (uint64, error) {
|
||||
err = thread.Step()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return thread.CurrentPC()
|
||||
}
|
||||
|
||||
ret := thread.ReturnAddressFromOffset(fde.ReturnAddressOffset(pc))
|
||||
for {
|
||||
pc, err = step()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !fde.Cover(pc) && pc != ret {
|
||||
err := thread.continueToReturnAddress(pc, fde)
|
||||
if err != nil {
|
||||
if _, ok := err.(InvalidAddressError); !ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
pc, _ = thread.CurrentPC()
|
||||
}
|
||||
|
||||
_, nl, _ := thread.Process.GoSymTable.PCToLine(pc)
|
||||
if nl != l {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (thread *ThreadContext) continueToReturnAddress(pc uint64, fde *frame.FrameDescriptionEntry) error {
|
||||
for !fde.Cover(pc) {
|
||||
// Our offset here is be 0 because we
|
||||
// have stepped into the first instruction
|
||||
// of this function. Therefore the function
|
||||
// has not had a chance to modify its' stack
|
||||
// and change our offset.
|
||||
addr := thread.ReturnAddressFromOffset(0)
|
||||
bp, err := thread.Break(uintptr(addr))
|
||||
if err != nil {
|
||||
if _, ok := err.(BreakPointExistsError); !ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
bp.temp = true
|
||||
// Ensure we cleanup after ourselves no matter what.
|
||||
defer thread.clearTempBreakpoint(bp.Addr)
|
||||
|
||||
for {
|
||||
err = thread.Continue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// We wait on -1 here because once we continue this
|
||||
// thread, it's very possible the scheduler could of
|
||||
// change the goroutine context on us, we there is
|
||||
// no guarantee that waiting on this tid will ever
|
||||
// return.
|
||||
wpid, _, err := trapWait(thread.Process, -1, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if wpid != thread.Id {
|
||||
thread = thread.Process.Threads[wpid]
|
||||
}
|
||||
pc, _ = thread.CurrentPC()
|
||||
if (pc - 1) == bp.Addr {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Takes an offset from RSP and returns the address of the
|
||||
// instruction the currect function is going to return to.
|
||||
func (thread *ThreadContext) ReturnAddressFromOffset(offset int64) uint64 {
|
||||
regs, err := thread.Registers()
|
||||
if err != nil {
|
||||
panic("Could not obtain register values")
|
||||
}
|
||||
|
||||
retaddr := int64(regs.Rsp) + offset
|
||||
data := make([]byte, 8)
|
||||
syscall.PtracePeekText(thread.Id, uintptr(retaddr), data)
|
||||
return binary.LittleEndian.Uint64(data)
|
||||
}
|
||||
|
||||
func (thread *ThreadContext) clearTempBreakpoint(pc uint64) error {
|
||||
if bp, ok := thread.Process.BreakPoints[pc]; ok {
|
||||
_, err := thread.Clear(bp.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset program counter to our restored instruction.
|
||||
regs, err := thread.Registers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
regs.SetPC(bp.Addr)
|
||||
return syscall.PtraceSetRegs(thread.Id, regs)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,315 +1,11 @@
|
||||
package proctl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
import "syscall"
|
||||
|
||||
"syscall"
|
||||
|
||||
"github.com/derekparker/delve/dwarf/frame"
|
||||
)
|
||||
|
||||
// ThreadContext represents a single thread of execution in the
|
||||
// traced program.
|
||||
type ThreadContext struct {
|
||||
Id int
|
||||
Process *DebuggedProcess
|
||||
Status *syscall.WaitStatus
|
||||
Regs *syscall.PtraceRegs
|
||||
func WriteMemory(tid int, addr uintptr, data []byte) (int, error) {
|
||||
return syscall.PtracePokeData(tid, addr, data)
|
||||
}
|
||||
|
||||
// Obtains register values from the debugged process.
|
||||
func (thread *ThreadContext) Registers() (*syscall.PtraceRegs, error) {
|
||||
err := syscall.PtraceGetRegs(thread.Id, thread.Regs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get registers %s", err)
|
||||
}
|
||||
|
||||
return thread.Regs, nil
|
||||
}
|
||||
|
||||
// Returns the current PC for this thread id.
|
||||
func (thread *ThreadContext) CurrentPC() (uint64, error) {
|
||||
regs, err := thread.Registers()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return regs.PC(), nil
|
||||
}
|
||||
|
||||
// PrintInfo prints out the thread status
|
||||
// including: PC, tid, file, line, and function.
|
||||
func (thread *ThreadContext) PrintInfo() error {
|
||||
pc, err := thread.CurrentPC()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, l, fn := thread.Process.GoSymTable.PCToLine(pc)
|
||||
if fn != nil {
|
||||
fmt.Printf("Thread %d at %#v %s:%d %s\n", thread.Id, pc, f, l, fn.Name)
|
||||
} else {
|
||||
fmt.Printf("Thread %d at %#v\n", thread.Id, pc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sets a software breakpoint at addr, and stores it in the process wide
|
||||
// break point table. Setting a break point must be thread specific due to
|
||||
// ptrace actions needing the thread to be in a signal-delivery-stop in order
|
||||
// to initiate any ptrace command. Otherwise, it really doesn't matter
|
||||
// as we're only dealing with threads.
|
||||
func (thread *ThreadContext) Break(addr uintptr) (*BreakPoint, error) {
|
||||
var (
|
||||
int3 = []byte{0xCC}
|
||||
f, l, fn = thread.Process.GoSymTable.PCToLine(uint64(addr))
|
||||
originalData = make([]byte, 1)
|
||||
)
|
||||
|
||||
if fn == nil {
|
||||
return nil, InvalidAddressError{address: addr}
|
||||
}
|
||||
|
||||
_, err := syscall.PtracePeekData(thread.Id, addr, originalData)
|
||||
if err != nil {
|
||||
fmt.Println("PEEK ERR")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bytes.Equal(originalData, int3) {
|
||||
return nil, BreakPointExistsError{f, l, addr}
|
||||
}
|
||||
|
||||
_, err = syscall.PtracePokeData(thread.Id, addr, int3)
|
||||
if err != nil {
|
||||
fmt.Println("POKE ERR")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
breakpoint := &BreakPoint{
|
||||
FunctionName: fn.Name,
|
||||
File: f,
|
||||
Line: l,
|
||||
Addr: uint64(addr),
|
||||
OriginalData: originalData,
|
||||
}
|
||||
|
||||
thread.Process.BreakPoints[uint64(addr)] = breakpoint
|
||||
|
||||
return breakpoint, nil
|
||||
}
|
||||
|
||||
// Clears a software breakpoint, and removes it from the process level
|
||||
// break point table.
|
||||
func (thread *ThreadContext) Clear(pc uint64) (*BreakPoint, error) {
|
||||
bp, ok := thread.Process.BreakPoints[pc]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("No breakpoint currently set for %#v", pc)
|
||||
}
|
||||
|
||||
if _, err := syscall.PtracePokeData(thread.Id, uintptr(bp.Addr), bp.OriginalData); err != nil {
|
||||
return nil, fmt.Errorf("could not clear breakpoint %s", err)
|
||||
}
|
||||
|
||||
delete(thread.Process.BreakPoints, pc)
|
||||
|
||||
return bp, nil
|
||||
}
|
||||
|
||||
func (thread *ThreadContext) Continue() error {
|
||||
// Check whether we are stopped at a breakpoint, and
|
||||
// if so, single step over it before continuing.
|
||||
regs, err := thread.Registers()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get registers %s", err)
|
||||
}
|
||||
|
||||
if _, ok := thread.Process.BreakPoints[regs.PC()-1]; ok {
|
||||
err := thread.Step()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not step %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return syscall.PtraceCont(thread.Id, 0)
|
||||
}
|
||||
|
||||
// Single steps this thread a single instruction, ensuring that
|
||||
// we correctly handle the likely case that we are at a breakpoint.
|
||||
func (thread *ThreadContext) Step() (err error) {
|
||||
regs, err := thread.Registers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bp, ok := thread.Process.BreakPoints[regs.PC()-1]
|
||||
if ok {
|
||||
// Clear the breakpoint so that we can continue execution.
|
||||
_, err = thread.Clear(bp.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset program counter to our restored instruction.
|
||||
regs.SetPC(bp.Addr)
|
||||
err = syscall.PtraceSetRegs(thread.Id, regs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not set registers %s", err)
|
||||
}
|
||||
|
||||
// Restore breakpoint now that we have passed it.
|
||||
defer func() {
|
||||
_, err = thread.Break(uintptr(bp.Addr))
|
||||
}()
|
||||
}
|
||||
|
||||
err = syscall.PtraceSingleStep(thread.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("step failed: %s", err.Error())
|
||||
}
|
||||
|
||||
_, _, err = wait(thread.Id, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step to next source line. Next will step over functions,
|
||||
// and will follow through to the return address of a function.
|
||||
// Next is implemented on the thread context, however during the
|
||||
// course of this function running, it's very likely that the
|
||||
// goroutine our M is executing will switch to another M, therefore
|
||||
// this function cannot assume all execution will happen on this thread
|
||||
// in the traced process.
|
||||
func (thread *ThreadContext) Next() (err error) {
|
||||
pc, err := thread.CurrentPC()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := thread.Process.BreakPoints[pc-1]; ok {
|
||||
pc-- // Decrement PC to account for BreakPoint
|
||||
}
|
||||
|
||||
_, l, _ := thread.Process.GoSymTable.PCToLine(pc)
|
||||
fde, err := thread.Process.FrameEntries.FDEForPC(pc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
step := func() (uint64, error) {
|
||||
err = thread.Step()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return thread.CurrentPC()
|
||||
}
|
||||
|
||||
ret := thread.ReturnAddressFromOffset(fde.ReturnAddressOffset(pc))
|
||||
for {
|
||||
pc, err = step()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !fde.Cover(pc) && pc != ret {
|
||||
err := thread.continueToReturnAddress(pc, fde)
|
||||
if err != nil {
|
||||
if _, ok := err.(InvalidAddressError); !ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
pc, _ = thread.CurrentPC()
|
||||
}
|
||||
|
||||
_, nl, _ := thread.Process.GoSymTable.PCToLine(pc)
|
||||
if nl != l {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (thread *ThreadContext) continueToReturnAddress(pc uint64, fde *frame.FrameDescriptionEntry) error {
|
||||
for !fde.Cover(pc) {
|
||||
// Our offset here is be 0 because we
|
||||
// have stepped into the first instruction
|
||||
// of this function. Therefore the function
|
||||
// has not had a chance to modify its' stack
|
||||
// and change our offset.
|
||||
addr := thread.ReturnAddressFromOffset(0)
|
||||
bp, err := thread.Break(uintptr(addr))
|
||||
if err != nil {
|
||||
if _, ok := err.(BreakPointExistsError); !ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
bp.temp = true
|
||||
// Ensure we cleanup after ourselves no matter what.
|
||||
defer thread.clearTempBreakpoint(bp.Addr)
|
||||
|
||||
for {
|
||||
err = thread.Continue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// We wait on -1 here because once we continue this
|
||||
// thread, it's very possible the scheduler could of
|
||||
// change the goroutine context on us, we there is
|
||||
// no guarantee that waiting on this tid will ever
|
||||
// return.
|
||||
wpid, _, err := trapWait(thread.Process, -1, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if wpid != thread.Id {
|
||||
thread = thread.Process.Threads[wpid]
|
||||
}
|
||||
pc, _ = thread.CurrentPC()
|
||||
if (pc - 1) == bp.Addr {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Takes an offset from RSP and returns the address of the
|
||||
// instruction the currect function is going to return to.
|
||||
func (thread *ThreadContext) ReturnAddressFromOffset(offset int64) uint64 {
|
||||
regs, err := thread.Registers()
|
||||
if err != nil {
|
||||
panic("Could not obtain register values")
|
||||
}
|
||||
|
||||
retaddr := int64(regs.Rsp) + offset
|
||||
data := make([]byte, 8)
|
||||
syscall.PtracePeekText(thread.Id, uintptr(retaddr), data)
|
||||
return binary.LittleEndian.Uint64(data)
|
||||
}
|
||||
|
||||
func (thread *ThreadContext) clearTempBreakpoint(pc uint64) error {
|
||||
if bp, ok := thread.Process.BreakPoints[pc]; ok {
|
||||
_, err := thread.Clear(bp.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset program counter to our restored instruction.
|
||||
regs, err := thread.Registers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
regs.SetPC(bp.Addr)
|
||||
return syscall.PtraceSetRegs(thread.Id, regs)
|
||||
}
|
||||
|
||||
return nil
|
||||
func ReadMemory(tid int, addr uintptr, data []byte) (int, error) {
|
||||
return syscall.PtracePeekData(tid, addr, data)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user