阅读 456

注册的pprof路由居然被干掉了?

问题

我叫王大X,事情是这样的,我在一个服务内引入了net/http/pprof包,然后用http的默认handler开启了一个http的服务器,想要看下服务运行的pprof,大概如下

package main

import (
	"context"
	"log"
	"net/http"
	"net/http/pprof"
	"os"
	"os/signal"
	"strconv"
	"syscall"
)
var _ = pprof.Index
var httpServer *http.Server

type PProfConf struct {
	Port int
}

func InitPProf(pprofConf PProfConf) {
	if pprofConf.Port > 0 {
		go func() {
			httpServer = &http.Server{Addr: ":" + strconv.Itoa(pprofConf.Port), Handler: nil}
			log.Print("try start pprof, listen on :", pprofConf.Port)
			signalChan := make(chan os.Signal)
			signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGUSR2, syscall.SIGSEGV, syscall.SIGKILL)
			go func() {
				err := httpServer.ListenAndServe()
				if err != nil {
					log.Print("init pprof error: " + err.Error())
					signalChan <- syscall.SIGKILL
				}
			}()
			<-signalChan
			log.Print("now stop http svr")
			httpServer.Shutdown(context.Background())
			log.Print("stop http successfully")
		}()
	}
}
复制代码

因为net/http/pprof包默认会注册pprof相关的路由,理论上只要引入了这个包,然后开启http服务器就能使用pprof了, 注册路由的代码在net/http/pprof/pprof.go:80

image.png

可就当我想要访问的时候,万万没想到,404了。。。

> curl -GET http://localhost:16339/debug/pprof/
404 page not found
复制代码

而服务是成功监听了http端口的,好家伙。。。

image.png

定位过程

先是想想有什么可能的原因,因为net/http/pprof包是在func init()中注册路由的,也就是说,注册路由之后,可能在其他文件的init()方法,对http做了点什么手脚导致我访问不到。

而我怀疑有以下可能

  • 服务里开启的http服务器,用的handler不是默认的http处理器http.DefaultServeMux
  • 路由加了莫名其妙的前缀
  • 路由被注销了

第一个猜想比较容易认证,既然认为handler不对,那用http.DefaultServeMux显式注册某个路由就好了,而事实证明,http服务器用的handler就是http.DefaultServeMux

httpServer = &http.Server{Addr: ":" + strconv.Itoa(pprofConf.Port), Handler: nil}
http.DefaultServeMux.HandleFunc("/debug/pprof/", pprof.Index)
log.Print("try start pprof, listen on :", pprofConf.Port)
signalChan := make(chan os.Signal)
// ... 以下省略
复制代码

这里显式注册的路由访问成功了,排除。

而二和三就难搞一点点,主要是http.DefaultServeMux 不支持把注册的路由打印出来,而且看了下http的源码,也没有对外暴露方法,以支持对注册过的路由进行修改。

image.png

到这里加日志和看源码就解决不了话,就需要引入高级调试工具了,比如在Go slice扩容深度分析中用到的gdb。而我在这里用的是Delve,delve是专门为Go设计开发的调试工具,很好使。。可以看官方的github,当然还有巨人的肩膀

使用delve来定位

既然是路由的问题,那就直接把断点打到注册的地方http.HandleFunc,毕竟如果让我出于什么考量对路由做点什么手脚,我也会调回这里注册点我自己的路由,开搞

# 在程序目录下执行
$ dlv debug
Type 'help' for list of commands.
# 运行程序
(dlv) r 
Process restarted with PID 24891
# 在注册路由的地方打上断点
(dlv) b /usr/local/Cellar/go/1.16.3/libexec/src/net/http/server.go:2497
Breakpoint 1 set at 0x16d04ef for net/http.(*ServeMux).HandleFunc() /usr/local/Cellar/go/1.16.3/libexec/src/net/http/server.go:2497
# 继续往下走
(dlv) c
> net/http.(*ServeMux).HandleFunc() /usr/local/Cellar/go/1.16.3/libexec/src/net/http/server.go:2497 (hits goroutine(1):1 total:1) (PC: 0x16d04ef)
  2492:         es[i] = e
  2493:         return es
  2494: }
  2495: 
  2496: // HandleFunc registers the handler function for the given pattern.
=>2497: func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
  2498:         if handler == nil {
  2499:                 panic("http: nil handler")
  2500:         }
  2501:         mux.Handle(pattern, HandlerFunc(handler))
  2502: }
# 打印路由的参数,以及堆栈信息,可以看到pprof的路由注册进去了
(dlv) p pattern
"/debug/pprof/"
(dlv) bt
 0  0x00000000016d04ef in net/http.(*ServeMux).HandleFunc
    at /usr/local/Cellar/go/1.16.3/libexec/src/net/http/server.go:2497
 1  0x00000000016d05cb in net/http.HandleFunc
    at /usr/local/Cellar/go/1.16.3/libexec/src/net/http/server.go:2513
 2  0x0000000001811995 in net/http/pprof.init.0
    at /usr/local/Cellar/go/1.16.3/libexec/src/net/http/pprof/pprof.go:84
 3  0x0000000001051dff in runtime.doInit
# 这里省略一部分无用的堆栈,直接到关键信息
.....
# 然后就真的发现了一个奇怪的路由!!! 
(dlv) p pattern
"/cmds"
# 打印堆栈,发现是一个叫trpc-go的家伙注册的
(dlv) bt
0  0x00000000016d04ef in net/http.(*ServeMux).HandleFunc
   at /usr/local/Cellar/go/1.16.3/libexec/src/net/http/server.go:2497
1  0x000000000181a4af in git.code.oa.com/trpc-go/trpc-go/admin.(*router).Config
   at /Users/siasyliang/go/pkg/mod/git.code.oa.com/trpc-go/trpc-go@v0.5.2/admin/router.go:49
2  0x0000000001816afa in git.code.oa.com/trpc-go/trpc-go/admin.init.0
   at /Users/siasyliang/go/pkg/mod/git.code.oa.com/trpc-go/trpc-go@v0.5.2/admin/admin.go:45
3  0x0000000001051dff in runtime.doInit
   at /usr/local/Cellar/go/1.16.3/libexec/src/runtime/proc.go:6265
4  0x0000000001051d6b in runtime.doInit
复制代码

于是用IDE追踪到代码,结果发现在admin.go这个文件,发现有这么一个东西,因为安全方面的考量,直接帮我把路由给干掉了,真就好家伙了。。。

image.png

pprof的路由居然还真的是被干掉的!

知其所以然

上面也提到,http.DefaultServeMux根本没有对外提供对已注册路由进行修改的方法,那是怎么做的呢?

可能已经有大哥想到了,没错,就是反射+unsafe。通过对http.DefaultServeMux做反射,根据fieldName拿到路由对应的field,然后用unSafe.Pointer拿到其指针,将其指针强转成map,并对map中的路由做delete操作,大概如下

v := reflect.ValueOf(http.DefaultServeMux)

// 删除map中的值
mField := v.Elem().FieldByName("m")
if !mField.IsValid() {
	return errors.New("http.DefaultServeMux does not have a field called `m`")
}
mPointer := unsafe.Pointer(mField.UnsafeAddr())
m := (*map[string]muxEntry)(mPointer)
for _, pattern := range patterns {
	delete(*m, pattern)
}
复制代码

于是就没有一点点防备,也没有一丝顾虑,就这样把我注册的pprof路由给干掉了,讲道理干掉也不输出点啥信息么。。。有点离谱

总结

笔者注册了pprof,但pprof访问不通

使用delve进行定位之后

发现有个包在init()方法中

使用反射+unsafe的办法把http.DefaultServeMux中注册过的pprof相关的路由全delete掉了。

学到了吧?再被问到能不能访问到struct里的私有变量,即便没有对外暴露方法,也记得回答下反射+unsafe。

附代码里提到的安全问题:github.com/golang/go/i…

欢迎交流~

文章分类
后端
文章标签