Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

syscall: ptrace weird behaviour #50920

Open
dzonerzy opened this issue Jan 31, 2022 · 3 comments
Open

syscall: ptrace weird behaviour #50920

dzonerzy opened this issue Jan 31, 2022 · 3 comments
Labels
WaitingForInfo

Comments

@dzonerzy
Copy link

@dzonerzy dzonerzy commented Jan 31, 2022

What version of Go are you using (go version)?

go version go1.17.6 linux/amd64

Does this issue reproduce with the latest release?

Need to test with latest version

What operating system and processor architecture are you using (go env)?

go env Output
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/dzonerzy/.cache/go-build"
GOENV="/home/dzonerzy/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/dzonerzy/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/dzonerzy/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.17.6"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/mnt/c/Users/DZONERZY/GolangProjects/test/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build1533898456=/tmp/go-build -gno-record-gcc-switches"

What did you do?

I have a binary and I replace some conditional jumps to HLT instruction which cause a SIGSEGV once executed , when the process start the first time the process run again itself but with different argument and start to trace the child , then in the ptrace loop the tracer handle all the SIGSEGV and replace the register with the correct values. Now this actually works fine but sometimes even if the child is uder ptrace (I confirmed that calling ptrace twice in the child and got operation not permitted) seems like the tracer can't catch the signals from the tracee , so the tracee actually crash with a panic (that should never happen in my opinion since the tracee was already being traced).

Maybe golang handle signals in a unique way , but since i'm tracing all the fork/exec + childs this should never happen anyway.

//go:build linux
// +build linux

package main

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/sha256"
	"encoding/base64"
	"encoding/binary"
	"encoding/gob"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"syscall"

	"github.com/dzonerzy/godis/internal/pkg/disassembler"
	"github.com/lunixbochs/struc"
)

const (
	CF_MASK = 1 << 1  // bit 1
	PF_MASK = 1 << 2  // bit 2
	AF_MASK = 1 << 4  // bit 4
	ZF_MASK = 1 << 6  // bit 6
	SF_MASK = 1 << 7  // bit 7
	OF_MASK = 1 << 11 // bit 11
)

type StolenBytesTable struct {
	Magic     uint64 `struc:"uint64,little"`
	Size      uint32 `struc:"uint32,sizeof=TableData"`
	TableData []byte `struc:"[]uint8"`
}

type modStolenEntry struct {
	OriginalBytes []byte
	Kind          uint16
	Displacement  int32
	Size          int
	Address       uint64
}

func check_eflags_mask(eflags uint64, mask uint64) bool {
	return (eflags & mask) == mask
}


func get_stolen_entry(addr uint64) *modStolenEntry {
    // this function actually return the original instruction at that address
    return nil
}

func debugger(pwd string, cmd *exec.Cmd) {
	if err := decryptTable(pwd, table.TableData); err != nil {
		log.Fatalf("Unable to decode table: %v", err)
	}
	if err := cmd.Start(); err != nil {
		log.Fatalf("unable to start program: %v", err)
	}
	err = cmd.Wait()
	log.Printf("State: %v\n", err)
	wpid := cmd.Process.Pid
	pgid, err := syscall.Getpgid(cmd.Process.Pid)
	if err != nil {
		log.Fatalf("Getpgid: %v", err)
	}
	syscall.PtraceSetOptions(-1, 0x100000|syscall.PTRACE_O_TRACECLONE|syscall.PTRACE_O_TRACEFORK|syscall.PTRACE_O_TRACEEXEC|syscall.PTRACE_O_TRACEVFORK)
	syscall.PtraceCont(wpid, 0)
	proc := cmd.Process
loop:
	for {
		wpid, err = syscall.Wait4(-1*pgid, &status, 0, nil)
		if err != nil {
			err = fmt.Errorf("error while waiting for process: %v", err)
			break
		}
		log.Println("Got Something ", status)
		if status.Exited() || status.Signaled() {
			log.Println("Process exited, code:", status.ExitStatus(), "Signal:", int(status.Signal()))
			err = nil
			break
		} else if status.Stopped() {
			log.Println("STOPPED")
			if proc.Pid == wpid && (status.StopSignal() == syscall.SIGSEGV) {
				var regs syscall.PtraceRegs
				if err = syscall.PtraceGetRegs(proc.Pid, &regs); err != nil {
					err = fmt.Errorf("error while getting registers: %v", err)
					break
				}
				log.Printf("Got SIGSEGV %x", regs.Rip)
				entry := get_stolen_entry(regs.Rip)
				if entry == nil {
					err = fmt.Errorf("unable to find stolen entry for address %x", regs.Rip)
					break
				} else {
					flags := regs.Eflags
					switch entry.Kind {
					case disassembler.I_JZ:
						log.Println("Handling JZ")
						if check_eflags_mask(flags, ZF_MASK) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JNZ:
						log.Println("Handling JNZ")
						if !check_eflags_mask(flags, ZF_MASK) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JS:
						log.Println("Handling JS")
						if check_eflags_mask(flags, SF_MASK) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JNS:
						log.Println("Handling JNS")
						if !check_eflags_mask(flags, SF_MASK) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JP:
						log.Println("Handling JP")
						if check_eflags_mask(flags, PF_MASK) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JNP:
						log.Println("Handling JNP")
						if !check_eflags_mask(flags, PF_MASK) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JO:
						log.Println("Handling JO")
						if check_eflags_mask(flags, OF_MASK) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JNO:
						log.Println("Handling JNO")
						if !check_eflags_mask(flags, OF_MASK) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JA:
						log.Println("Handling JA")
						if !check_eflags_mask(flags, CF_MASK) && !check_eflags_mask(flags, ZF_MASK) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JAE:
						log.Println("Handling JAE")
						if !check_eflags_mask(flags, CF_MASK) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JB:
						log.Println("Handling JB")
						if check_eflags_mask(flags, CF_MASK) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JBE:
						log.Println("Handling JBE")
						if check_eflags_mask(flags, CF_MASK) && check_eflags_mask(flags, ZF_MASK) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JG:
						log.Println("Handling JG")
						if !check_eflags_mask(flags, ZF_MASK) && (check_eflags_mask(flags, SF_MASK) == check_eflags_mask(flags, OF_MASK)) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JGE:
						log.Println("Handling JGE")
						if check_eflags_mask(flags, SF_MASK) == check_eflags_mask(flags, OF_MASK) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JL:
						log.Println("Handling JL")
						if check_eflags_mask(flags, SF_MASK) != check_eflags_mask(flags, OF_MASK) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JLE:
						log.Println("Handling JLE")
						if check_eflags_mask(flags, ZF_MASK) || (check_eflags_mask(flags, SF_MASK) != check_eflags_mask(flags, OF_MASK)) {
							regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
						} else {
							regs.Rip = (entry.Address + uint64(entry.Size))
						}
					case disassembler.I_JMP, disassembler.I_JMP_FAR:
						log.Println("Handling JMP")
						regs.Rip = (entry.Address + uint64(entry.Displacement) + uint64(entry.Size))
					default:
						err = fmt.Errorf("conditional jump not implemented")
						break loop
					}
					if err = syscall.PtraceSetRegs(proc.Pid, &regs); err != nil {
						err = fmt.Errorf("error while setting registers: %v", err)
						break
					}
				}
				if err = syscall.PtraceCont(proc.Pid, 0); err != nil {
					err = fmt.Errorf("error while sending continue signal: %v", err)
					break
				}
			} else if proc.Pid == wpid && (status.StopSignal() == syscall.SIGTRAP) {
				log.Printf("Got SIGTRAP")
				if err = syscall.PtraceCont(proc.Pid, 0); err != nil {
					log.Printf("Error while sending continue signal: %v", err)
					break
				}
			} else {
				if err = syscall.PtraceCont(proc.Pid, int(status.StopSignal())); err != nil {
					log.Printf("Error while sending continue signal: %v", err)
					break
				}
			}
		}
	}
	if err != nil {
		proc.Kill()
		log.Fatal(err)
	}
}

func install() {
	var found bool = false
	var index int = -1
	for i, arg := range os.Args {
		if arg == "gonfusion_nanomites" {
			found = true
			index = i
		}
	}
	if !found {
		os.Args = append(os.Args, "restart")
		cmd := exec.Command(os.Args[0], os.Args[1:]...)
		cmd.Env = os.Environ()
		cmd.Stderr = os.Stderr
		cmd.Stdout = os.Stdout
		cmd.Stdin = os.Stdin
		cmd.SysProcAttr = &syscall.SysProcAttr{Ptrace: true}
		debugger(pwd, cmd)
		os.Exit(0)
	} else {
		os.Args = os.Args[0:index]
	}
}

// Just an example
func main() {
   install()
   // ....
   // ....
   // Code below containing HLT instruction
}

What did you expect to see?

I expected to be able to catch SIGSEGV signal everytime since signals should be handled by the tracer and then passed to the tracee.

What did you see instead?

The tracee actually process the signal SIGSEGV and panic

@dzonerzy dzonerzy changed the title affected/package: syscall ptrace weird behaviour Jan 31, 2022
@dzonerzy dzonerzy changed the title syscall ptrace weird behaviour syscall: ptrace weird behaviour Jan 31, 2022
@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Jan 31, 2022

I don't really see how anything that a Go program could do would affect the ptrace behavior. The only interesting thing that I can think of is that Go programs are always multi-threaded, and I don't know how ptrace handles multi-threaded programs.

If you can tell us something to change in Go, then we can consider changing it. But right now I have no idea how to make any progress on this issue. Sorry.

@ianlancetaylor ianlancetaylor added the WaitingForInfo label Jan 31, 2022
@dzonerzy
Copy link
Author

@dzonerzy dzonerzy commented Jan 31, 2022

As the ptrace documentation states, with PTRACE_SETOPTIONS is possible to specify few flags like:

  1. PTRACE_O_TRACECLONE used to intercept clone , clone is actually used in pthread_create so this way i'm able to catch new threads
  2. PTRACE_O_TRACEFORK this is used to block the tracer on the next call to fork in the tracee
  3. PTRACE_O_TRACEEXEC same buf for exec* family (execve, execl and so on..)
  4. PTRACE_O_TRACEVFORK same but for vfork

Now this should be enough to cover an usual go program execution (main thread, goroutines, exec and so on..).
Regarding signals kernel should send those signals first to the tracer and then the tracer can send those signals to the tracee or just ignore them (signal injection & signal suppression) , so I really can't figure out why sometimes the tracee receive the signal before its tracer.

@dzonerzy
Copy link
Author

@dzonerzy dzonerzy commented Feb 1, 2022

I have made a testcase reproducer, the following code does the exact same thing I described:

package main

import (
	"fmt"
	"log"
	"os"
	"os/exec"
	"syscall"
	"time"
	"unsafe"
)

func callpayload(data []byte) {
	missing := len(data)
	pagesize := os.Getpagesize()
	if (pagesize-1)&missing > 0 {
		missing = ((missing + pagesize) & (^(pagesize - 1))) - missing
	}
	padding := make([]byte, missing)
	data = append(data, padding...)
	if err := syscall.Mprotect(data, syscall.PROT_READ|syscall.PROT_EXEC|syscall.PROT_READ); err != nil {
		log.Fatal(err)
	}
	fnc_ptr := &data
	fncptr := unsafe.Pointer(&fnc_ptr)
	fnc_type := *(*func())(fncptr)
	fnc_type()
}

func debugger(cmd *exec.Cmd) {
	var status syscall.WaitStatus
	if err := cmd.Start(); err != nil {
		log.Fatal(err)
	}
	_ = cmd.Wait()
	wpid := cmd.Process.Pid
	var err error
	syscall.PtraceSetOptions(-1, 0x100000|syscall.PTRACE_O_TRACECLONE|syscall.PTRACE_O_TRACEFORK|syscall.PTRACE_O_TRACEEXEC|syscall.PTRACE_O_TRACEVFORK)
	syscall.PtraceCont(wpid, 0)
	proc := cmd.Process
	for {
		wpid, err = syscall.Wait4(-1, &status, syscall.WALL, nil)
		if err != nil {
			err = fmt.Errorf("error while waiting for process: %v", err)
			break
		}
		if status.Exited() || status.Signaled() {
			log.Println("CHILD EXITED")
			err = nil
			break
		} else if status.Stopped() {
			if proc.Pid == wpid && (status.StopSignal() == syscall.SIGSEGV) {
				var regs syscall.PtraceRegs
				if err = syscall.PtraceGetRegs(proc.Pid, &regs); err != nil {
					err = fmt.Errorf("error while getting registers: %v", err)
					break
				}
				log.Printf("Got SIGSEGV %x", regs.Rip)
				// Skil the actual hlt instruction so the execution should continue
				regs.Rip += 1
				if err = syscall.PtraceSetRegs(proc.Pid, &regs); err != nil {
					err = fmt.Errorf("error while setting registers: %v", err)
					break
				}
			}
			if err = syscall.PtraceCont(proc.Pid, 0); err != nil {
				err = fmt.Errorf("error while sending continue signal: %v", err)
				break
			}
		} else if proc.Pid == wpid && (status.StopSignal() == syscall.SIGTRAP) {
			log.Println("SIGTRAP!")
			if err = syscall.PtraceCont(proc.Pid, 0); err != nil {
				log.Printf("Error while sending continue signal: %v", err)
				break
			}
		} else {
			if err = syscall.PtraceCont(proc.Pid, int(status.StopSignal())); err != nil {
				log.Printf("Error while sending continue signal: %v", err)
				break
			}
		}

	}
	if err != nil {
		proc.Kill()
		log.Fatal(err)
	}
	os.Exit(0)
}

func install() {
	var found bool = false
	var index int = -1
	for i, arg := range os.Args {
		if arg == "test" {
			found = true
			index = i
		}
	}
	if !found {
		os.Args = append(os.Args, "test")
		cmd := exec.Command(os.Args[0], os.Args[1:]...)
		cmd.Env = os.Environ()
		cmd.Stderr = os.Stderr
		cmd.Stdout = os.Stdout
		cmd.Stdin = os.Stdin
		cmd.SysProcAttr = &syscall.SysProcAttr{Ptrace: true}
		debugger(cmd)
	} else {
		os.Args = os.Args[0:index]
	}
}

func main() {
	// runtime.LockOSThread() if I use lock os thread the tracer is always able to catch SIGSEGV
	// install the debugger on the parent process, and does nothing on the tracee
	install()
	// call a fake function with hlt instruction
	counter := 0
	for counter < 5 {
		fnc := []byte{0x55, 0x48, 0x89, 0xe5, 0xf4, 0x5d, 0xc3} // 0xf4 = HLT
		callpayload(fnc)
		counter++
		time.Sleep(10 * time.Microsecond)
	}
	fmt.Println("If you can see this the tracer worked just fine")
}

That's the output of a normal execution flow:

2022/02/01 02:23:23 Got SIGSEGV c000117004
2022/02/01 02:23:23 Got SIGSEGV c000119004
2022/02/01 02:23:23 Got SIGSEGV c00011b004
2022/02/01 02:23:23 Got SIGSEGV c00011d004
2022/02/01 02:23:23 Got SIGSEGV c00011f004
If you can see this the tracer worked just fine
2022/02/01 02:23:23 CHILD EXITED

Basically each SIGSEGV the signal is sent to the parent process (the tracer) which suppress it calling ptrace(PTRACE_CONT, pid, 0, 0), now this should be the default behaviour of the program however sometimes this happen:

2022/02/01 03:19:57 Got SIGSEGV c0000ad004
2022/02/01 03:19:57 Got SIGSEGV c0000af004
2022/02/01 03:19:57 Got SIGSEGV c0000b1004 <- first 3 SIGSEGV are handled correctly
unexpected fault address 0x0
fatal error: fault
[signal SIGSEGV: segmentation violation code=0x80 addr=0x0 pc=0xc00006d004]

goroutine 1 [running]:
runtime.throw({0x4b8a00, 0x47b68d})
	/usr/local/go/src/runtime/panic.go:1198 +0x71 fp=0xc000086e50 sp=0xc000086e20 pc=0x4307f1
runtime: unexpected return pc for runtime.sigpanic called from 0xc00006d004
stack: frame={sp:0xc000086e50, fp:0xc000086ea0} stack=[0xc000086000,0xc000087000)
0x000000c000086d50:  0x00007f8af46742b0  0x000000c000086d90 
0x000000c000086d60:  0x0000000000431eae <runtime.recordForPanic+0x000000000000004e>  0x0000000000000000 
0x000000c000086d70:  0x000000c000086db0  0x00000000004321bf <runtime.gwrite+0x00000000000000ff> 
0x000000c000086d80:  0x0000000000000001  0x0000000000000000 
0x000000c000086d90:  0x000000c000086dd0  0x00000000004321bf <runtime.gwrite+0x00000000000000ff> 
0x000000c000086da0:  0x0000000000000002  0x00000000004c0cd8 
0x000000c000086db0:  0x0000000000000001  0x0000000000000001 
0x000000c000086dc0:  0x000000c000086e3d  0x0000000000000003 
0x000000c000086dd0:  0x000000c000086e20  0x000000000045bc2e <runtime.systemstack+0x000000000000002e> 
0x000000c000086de0:  0x0000000000430a30 <runtime.fatalthrow+0x0000000000000050>  0x000000c000086df0 
0x000000c000086df0:  0x0000000000430a60 <runtime.fatalthrow.func1+0x0000000000000000>  0x000000c0000001a0 
0x000000c000086e00:  0x00000000004307f1 <runtime.throw+0x0000000000000071>  0x000000c000086e20 
0x000000c000086e10:  0x000000c000086e40  0x00000000004307f1 <runtime.throw+0x0000000000000071> 
0x000000c000086e20:  0x000000c000086e28  0x0000000000430820 <runtime.throw.func1+0x0000000000000000> 
0x000000c000086e30:  0x00000000004b8a00  0x0000000000000005 
0x000000c000086e40:  0x000000c000086e90  0x0000000000445d36 <runtime.sigpanic+0x00000000000002f6> 
0x000000c000086e50: <0x00000000004b8a00  0x000000000047b68d <syscall.Mprotect+0x000000000000004d> 
0x000000c000086e60:  0x000000000000000a  0x000000c00006d000 
0x000000c000086e70:  0x0000000000000000  0x0000000000000005 
0x000000c000086e80:  0x0000000000000000  0x0000000000000005 
0x000000c000086e90:  0x000000c000086ea0 !0x000000c00006d004 
0x000000c000086ea0: >0x000000c000086f10  0x000000000049cb5e <main.callpayload+0x000000000000013e> 
0x000000c000086eb0:  0x000000c00006d000  0x000000c000086f51 
0x000000c000086ec0:  0x0000000000000000  0x0000000000000000 
0x000000c000086ed0:  0x000000c000086f10  0x0000000000000ff9 
0x000000c000086ee0:  0x0000000000001000  0x0000000000001000 
0x000000c000086ef0:  0x000000c00006d000  0x000000c00006c000 
0x000000c000086f00:  0x000000c0000a8050  0x000000c0000001a0 
0x000000c000086f10:  0x000000c000086f70  0x000000000049d365 <main.main+0x0000000000000085> 
0x000000c000086f20:  0x000000c00006d000  0x0000000000001000 
0x000000c000086f30:  0x0000000000001000  0x0000000000000000 
0x000000c000086f40:  0x0000000000033089  0x0000000000000000 
0x000000c000086f50:  0xc35df4e589485500  0x0000000000000003 
0x000000c000086f60:  0x00000000004a56a0  0x000000c0000001a0 
0x000000c000086f70:  0x000000c000086fd0  0x0000000000432ec7 <runtime.main+0x0000000000000227> 
0x000000c000086f80:  0x000000c000080000  0x0000000000000000 
0x000000c000086f90:  0x0000000000000000  0x0000000000000000 
runtime.sigpanic()
	/usr/local/go/src/runtime/signal_unix.go:742 +0x2f6 fp=0xc000086ea0 sp=0xc000086e50 pc=0x445d36
2022/02/01 03:19:57 CHILD EXITED  <- notice the tracer was able to find out that the tracee exited (by a sigpanic)

Also seems like runtime.LockOSThread prevent this behaviour from happening, still can't find the root cause this stuff is driving me crazy.

@seankhliao seankhliao added WaitingForInfo and removed WaitingForInfo labels Jun 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
WaitingForInfo
Projects
None yet
Development

No branches or pull requests

3 participants