Why be careful with defer in Golang

When does defer function executes?

The standard answer is: defer runs a function before the enclosing function returns.

It is a wrong explanation. The defer executes as follows:

  1. The enclosing function has processed returned values and stored them in registers
  2. All defer functions run in a LIFO order of definition.
  3. If the return parameters are named, the defer function can alter the return values.

Short Examples on Vulnerabilities in defer Code

defer Modifying Named Return Parameters

package main

import (
	"fmt"
	"sync"
)

func test4(x int) (y int) {
	defer func() {
		y = 100
	}()

	if x == 123 {
		return x
	}

	defer func() {
		y = 200
	}()

	return x
}

func main() {
	fmt.Printf("Hello x=%v\n", test4(1234))
}

The above code returns value 123 referenced by y. Now, defer functions start executing. Mind it, the function test4() is yet to return to main().

  • The last defer function modifies the return value y to 200.
  • The first defer function modifies the return value y to 100.
  • test4() returns 100.

Deadlocked Program with Improper Understanding of defer

func test5(abort bool) {
	var mutex = &sync.Mutex{}

	defer func() {
		fmt.Printf("locking the mutex-first\n")
		mutex.Unlock()
	}()

	if abort {
		mutex.Lock()
		fmt.Println("critical section")
		mutex.Unlock()
		return
	}

	defer func() {
		fmt.Printf("locking the mutex-second\n")
		mutex.Lock()
	}()

}

func main() {
	test5(true)
}
go run deferDetails/main.go
critical section
locking the mutex-first
fatal error: sync: unlock of unlocked mutex

goroutine 1 [running]:
runtime.throw(0x10d2d55, 0x1e)
        /Users/ovo/.goenv/versions/1.14.0/src/runtime/panic.go:1112 +0x72 fp=0xc000078e38 sp=0xc000078e08 pc=0x102e542
sync.throw(0x10d2d55, 0x1e)
        /Users/ovo/.goenv/versions/1.14.0/src/runtime/panic.go:1098 +0x35 fp=0xc000078e58 sp=0xc000078e38 pc=0x102e4c5
sync.(*Mutex).unlockSlow(0xc00018c008, 0xc0ffffffff)
        /Users/ovo/.goenv/versions/1.14.0/src/sync/mutex.go:196 +0xd6 fp=0xc000078e80 sp=0xc000078e58 pc=0x106a746
sync.(*Mutex).Unlock(...)
        /Users/ovo/.goenv/versions/1.14.0/src/sync/mutex.go:190
main.test5.func1(0xc00018c008)
        /Users/ovo/go/1.14.0/src/mygo/deferDetails/main.go:13 +0x8b fp=0xc000078ee0 sp=0xc000078e80 pc=0x109ec7b
main.test5(0xc00006c001)
        /Users/ovo/go/1.14.0/src/mygo/deferDetails/main.go:20 +0x11a fp=0xc000078f70 sp=0xc000078ee0 pc=0x109eb1a
main.main()
        /Users/ovo/go/1.14.0/src/mygo/deferDetails/main.go:31 +0x26 fp=0xc000078f88 sp=0xc000078f70 pc=0x109ebd6
runtime.main()
Explanation

ha! the defer caused a panic in the program. I am not quite sure why it failed. If I remove the return, the code works.

package main

import (
	"fmt"
	"sync"
)

func test5(abort bool) {
	var mutex = &sync.Mutex{}

	defer func() {
		fmt.Printf("locking the mutex-first\n")
		mutex.Unlock()
	}()

	if abort {
		mutex.Lock()
		fmt.Println("critical section")
		mutex.Unlock()
	}

	defer func() {
		fmt.Printf("locking the mutex-second\n")
		mutex.Lock()
	}()

}

func main() {
	test5(true)
}
go run deferDetails/main.go
critical section
locking the mutex-second
locking the mutex-first

Conclusion

  1. Use defer very carefully, especially around named return parameter
  2. Multiple defer statements are executed in reverse order of appearance.
  3. Need to root cause why the code with return panic.

References

Written with StackEdit.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: