Monkey patching net/http
Why?
As a member and moderator of the Discord Gophers server we try to help people with their questions about Go. Most of the time these questions are fairly usual: How do goroutines work? Why does my application crash? But over the last couple of months we have had new people come in and ask if they can easily change the header order of net/http. The answer has always been no, you would have to fork Go and change the net/http library yourself. After a while the other Gophers in the server were thinking of other possibilities. It has been on the back burner for a while, until we read the excellent blog post written by Bouke about monkey patching in go.
What is this monkey you speak of?
Monkey patching in short is replacing existing code with something else while the application is running. In the ruby world, and in some other dynamic languages, this is used to make testing easier in certain cases. It can also be used to add new methods to existing classes.
In Go this was thought to be impossible as it is a statically typed language. The language spec simply prevents us messing around with the code during runtime. Or so we thought! Bouke has found a way to do this in Go by using a couple of clever tricks to change the function Go executes when a function is called. The blog post does a great job of explaining how it works, so I won’t go into detail here.
The hack part I
So Go can be monkey patched. Let’s throw some patches at net/http and
call it a day. There is no way to change the types of the header field.
It is defined as type Header map[string][]string so we’ll have to
make that work. Doad came up with the idea
of using a single entry in the header map to store the other headers as
well. If the ‘single’ header had newlines in them they would be properly
sent as multiple headers. This normally won’t work as newlines are not
allowed in the header value. net/http will return an error if
you try:
Get "https://sven.wiltink.dev": net/http: invalid header field value "SomeValue\
nOtherHeader: OtherValue" for key SomeHeader
This pesky validation has to go away! It is performed in the Transport layer by calling httpguts.ValidHeaderFieldValue. The patch target has been found! Trying to patch this results in the following code:
The patch is in place and the code run, disappointingly yielding the same error.
Get "https://sven.wiltink.dev": net/http: invalid header field value "SomeValue\
nOtherHeader: OtherValue" for key SomeHeader
This doesn’t add up, the function was patched! So why isn’t the patched function called? By adding this snippet of code in net/http and our main we can see the function pointers are in fact different:
fmt.Printf("pointer in net/http: %d\n", reflect.ValueOf(httpguts.ValidHeaderFieldValue).Pointer())
fmt.Printf("pointer in main: %d\n", reflect.ValueOf(httpguts.ValidHeaderFieldValue).Pointer())
pointer in main: 6554096
pointer in net/http: 6228976
The detective work
Somehow the standard library calls a different ‘instance’ of the function that we are trying
to patch. Which one is it and why does it have a different address? Using readelf we can dump
the symbols in the binary. After converting the pointer to hex I found the following:
readelf -a -W banaan | grep -i 5f0bf0
6610: 00000000005f0bf0 60 FUNC GLOBAL DEFAULT 1 vendor/golang.org/x/net/http/httpguts.ValidHeaderFieldValue
net/http is calling a function that is prefixed with “vendor”. It turns out Go vendors the /x/ packages it needs in the standard library. The function we are patching isn’t the same ‘instance’ of the function. The vendored version can be found here. We don’t normally have access to this function from within our application, but there is a hacky way.
Linkname enters the chat
The go compiler toolchain has a special tool that enables us to define a function and link it
to a different implementation. This is called linkname and is used by some parts of the standard
library to call functions without importing a package. An example of this is the sync package, which
wants to call the unexported runtime function nanotime. Unexported functions can’t be called outside
the package where they are defined, so instead the sync package defines a function stub
and the runtime package uses linkname to link the two functions together.
The behaviour of linkname and other //go: comments is explained in the compile command documentation
//go:linkname localname [importpath.name]
This special directive does not apply to the Go code that follows it. Instead, the //go:linkname directive instructs the compiler to use “importpath.name” as the object file symbol name for the variable or function declared as “localname” in the source code. If the “importpath.name” argument is omitted, the directive uses the symbol’s default object file symbol name and only has the effect of making the symbol accessible to other packages. Because this directive can subvert the type system and package modularity, it is only enabled in files that have imported “unsafe”.
In the previous section the name of the vendored function was discovered. linkname can
be used to ‘magically’ have access to it. Using the code below we can demonstrate the pointer of this
new function is the same as the one used by net/http:
pointer in main: 6228976
pointer in net/http: 6228976
There is now a function called linknameMagic that shares the function pointer of the vendored
function! Because linknameMagic is no different from other functions from the perspective of
our code it can also be monkey patched:
pointer in main: 6228976
pointer in net/http: 6228976
the patched function was called!
the patched function was called!
the patched function was called!
the patched function was called!
the patched function was called!
<nil>
Note how performing the request also does not return an error anymore. The net/http library
has been fooled! When inspecting the http request the headers are however still on a single line
and not newline separated as they should. This is because the transport code of net/http
replaces all newlines with spaces. There is nothing that stops us from monkey patching that
as well, but I will be leaving that as an exercise for the reader.
Signing off,
Sven “CursedLinkname” Wiltink