golang 下对时间漂移的故障注入
golang 下对时间漂移的故障注入
仙雾很多时候我们在做测试时候,需要对程序的时间或者定时器进行一些操纵,各个语言对于获取时间和定时器的实现都不尽相同,在网上找了一下,发现有一个chaos-mesh的项目里面提供了一个对golang和rust程序注入故障的方案。
要想能针对某个进程进行时间漂移的注入,首先要知道这个进程里面对于获取时间的调用是怎么进行的。对于golang程序而言,当我们调用time.Now()时,实际上是利用了vDSO (virtual dynamic shared object)机制,该机制可以让一些诸如gettimeofday
、clock_gettime
的系统调用更快,而golang的time.Now()或者golang的定时器实现中维护堆时使用的就是clock_gettime
。
知道了调用的函数,接下来就是需要想办法修改掉它,这里chaos-mesh使用的方案是通过ptrace去修改对应进程的内存空间,将clock_gettime
的跳转改成一个自己的实现。
chaos-mesh还提供了一个测试程序的封装叫watchmaker,这个程序可以方便我们直接运行,只要直接输入pid和偏移量就可以完成注入。
主要功能的代码是func ModifyTime(pid int, deltaSec int64, deltaNsec int64, clockIdsMask uint64) error
大体流程如下
1. 利用runtime.LockOSThread(),将当期goroutine和linux的thread绑定,这个我猜应该是防止该goroutine被切走,ptrace失效。
2. ptrace.Trace(pid)
attach到pid上。通过/proc/pid/task
获取该进程的所有threads,并通过PtraceAttach syscall attach到每个tid上,同时通过/proc/pid/maps
拿到该进程的虚拟内存映射信息。
3. 通过关键字[vdso]
找到vDSO entry的起始地址。
4. 准备好一个fakeImage
,里面直接写好了我们的跳转的程序,最后24 bytes是我们的三个参数。这块涉及汇编,没仔细看fakeImage
里面的实现。
5. 在当前进程查询到是否曾经注入过我们的fakeImage
,如果没有那么通过mmap新map进去我们的fakeImage
,否则找到我们之前的entry。把最后24 bytes改写为当前设置的参数。
6. 找到vDSO entry中的clock_gettime
的原始地址,这里需要利用golang的debug/elf包,来找到对应的symbol address。
7. 把vDSO entry中的clock_gettime
跳转修改掉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14// JumpToFakeFunc writes jmp instruction to jump to fake function
func (p *TracedProgram) JumpToFakeFunc(originAddr uint64, targetAddr uint64) error {
instructions := make([]byte, 16)
// mov rax, targetAddr;
// jmp rax ;
instructions[0] = 0x48
instructions[1] = 0xb8
binary.LittleEndian.PutUint64(instructions[2:10], targetAddr)
instructions[10] = 0xff
instructions[11] = 0xe0
return p.PtraceWriteSlice(originAddr, instructions)
}
我们可以写个程序测试下效果
1 | // go build -o test test.go |
为了观察方便,同时起两个test进程,一个带参数,一个不带。./test & ./test x &
如果正常运行,那么输出结果会是这样的:
1 | 99335 start count 2021-04-26 15:14:40.290945165 +0800 CST m=+0.000102053 |
我们先等到99334进程触发了一次定时器超时之后,我们立刻通过watchmaker
工具注入一个30秒的漂移。sudo ./bin/watchmaker -pid 99334 -sec_delta 30 -nsec_delta 0 -clk_ids "CLOCK_REALTIME,CLOCK_MONOTONIC"
注意如果我们只是想影响程序中time.Now(),只修改CLOCK_REALTIME
就行了。这里我们需要影响定时器的超时,根据golang的定时器实现,定时器数据结构中取时间使用的是runtime·nanotime1
,而对应的汇编实现里面取的是CLOCK_MONOTONIC
,所以我们修改的clockid除了CLOCK_REALTIME
还需要包含CLOCK_MONOTONIC
。
程序会输出:
1 | watchmaker Version: version.Info{GitVersion:"master-ge885846a41fd88", GitCommit:"e885846a41fd88aa3a46f8b318321b8e889312b9", BuildDate:"2021-04-13T08:09:23Z", GoVersion:"go1.14.14", Compiler:"gc", Platform:"linux/amd64"} |
这里也可以看到,一个普通的go程序也会启动很多线程。。。
然后回去看我们测试进程的输出变成了这样:
1 | 99335 tick 2021-04-26 15:15:10.291344814 +0800 CST m=+30.000501519 |
注意golang的默认的time这个数据结构的打印,m=+后面的数字就是进程启动后的monotinic时间,这里99334进程的monotinic时间一下子从38变成了68,所以立马触发了第二次超时。
至此,我们就完成了对一个golang进程的时间漂移的注入,赶快去测试下生产代码中是否有问题吧,据chaos-mesh团队的分享中所说,很多开源项目均有或大或小的问题哦。