Vulnerability analysis of Golang applications and more with Red Hat CodeReady Dependency Analytics v0.3.2

This article focuses on some weird Go slice behaviors that the developer might encounter without adequate knowledge of slice internals.

I will begin by illustrating the problem with a fictional company called ACME Corporation and its software engineer, Wile E. Then I will explain why that happened and how to avoid it.

Wile meets the Go slices

During the company's day of learning, Wile decides to learn the Go language. He read about arrays and slices and wants to do some practice, beginning with a straightforward exercise—a piece of code that:

  • creates a slice containing all the digits from 1 to 0
  • derives from the previous one, another slice containing all the digits from 1 to 9.

Wile thinks: "Wow, that's really easy! Let's try some code!"

He starts writing the code and ends up with the first exercise:

package main

import "fmt"

func main() {
	// STEP1: create the first slice with the digit from 1 to 0
	fmt.Println("[STEP1] Create a slice containing all the decimal digits (1,2,3,4,5,6,7,8,9,0)")
	charSlice1 := []byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}
	fmt.Println("[STEP1]", "charSlice1", string(charSlice1))

	fmt.Println("\n[STEP2] Create a slice containing all digits from the first slice but the last one")
	// STEP2: create a new slice with all the digits from slice 1 but the last one (the 0)
	charSlice2 := charSlice1[:len(charSlice1)-1]
	fmt.Println("[STEP2]", "charSlice2", string(charSlice2))

	fmt.Println("\nFinal Result:")
	// Print both slices
	fmt.Println("charSlice1", string(charSlice1))
	fmt.Println("charSlice2", string(charSlice2))
}

He runs the code and gets the following output:

[STEP1] Create a slice containing all the decimal digits (1,2,3,4,5,6,7,8,9,0)
[STEP1] charSlice1 1234567890

[STEP2] Create a slice containing all digits from the first slice but the last one
[STEP2] charSlice2 123456789

Final Result:
charSlice1 1234567890
charSlice2 123456789

Wile thinks: "Well, that's been easy! From what I learned, a slice is just a view on a backing array, so charSlice1 and charSlice2 are pointing to the same backing array. Let's try to change the content of one: if that's true, the other one will also change!"

Wile ends up with the following code:

package main

import "fmt"

func main() {
	// STEP1: create the first slice with the digit from 1 to 0
	fmt.Println("[STEP1] Create a slice containing all the decimal digits (1,2,3,4,5,6,7,8,9,0)")
	charSlice1 := []byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}
	fmt.Println("[STEP1]", "charSlice1", string(charSlice1))

	// STEP2: create a new slice with all the digits from slice 1 but the last one (the 0)
	fmt.Println("\n[STEP2] Create a slice containing all digits from the first slice but the last one")
	charSlice2 := charSlice1[:len(charSlice1)-1]
	fmt.Println("[STEP2]", "charSlice2", string(charSlice2))

	// STEP3: change the numbers 3 and 4 with an X in charSlice2
	fmt.Println("\n[STEP3] Changing the numbers 3 and 4 to the 'X' character in charSlice2")
	charSlice2[2] = 'X'
	charSlice2[3] = 'X'
	fmt.Println("[STEP3]", "charSlice2", string(charSlice2))

	// Print both slices
	fmt.Println("\nFinal Result:")
	fmt.Println("charSlice1", string(charSlice1))
	fmt.Println("charSlice2", string(charSlice2))
}

He runs it and gets the following output:

[STEP1] Create a slice containing all the decimal digits (1,2,3,4,5,6,7,8,9,0)
[STEP1] charSlice1 1234567890

[STEP2] Create a slice containing all digits from the first slice but the last one
[STEP2] charSlice2 123456789

[STEP3] Changing the numbers 3 and 4 to the 'X' character in charSlice2
[STEP3] charSlice2 12XX56789

Final Result:
charSlice1 12XX567890
charSlice2 12XX56789

As expected, changing charSlice2 changed charSlice1 too.

Wile thinks: "That's too easy. I'm starting to love Go! Now I want to add a fourth step that appends a value to the slices."

After a few seconds, Wile ends up with the snippet below:

package main

import "fmt"

func main() {
	// STEP1: create the first slice with the digit from 1 to 0
	fmt.Println("[STEP1] Create a slice containing all the decimal digits (1,2,3,4,5,6,7,8,9,0)")
	charSlice1 := []byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}

	// STEP2: create a new slice with all the digits from slice 1 but the last one (the 0)
	fmt.Println("\n[STEP2] Create a slice containing all digits from the first slice but the last one")
	charSlice2 := charSlice1[:len(charSlice1)-1]

	fmt.Println("\n[STEP2] Result:")
	fmt.Println("[STEP2]", "charSlice1", string(charSlice1))
	fmt.Println("[STEP2]", "charSlice2", string(charSlice2))

	// STEP3: change the numbers 3 and 4 with an X in charSlice2
	fmt.Println("\n[STEP3] Changing the numbers 3 and 4 to the 'X' character in charSlice2")
	charSlice2[2] = 'X'
	charSlice2[3] = 'X'

	fmt.Println("\n[STEP3] Result:")
	fmt.Println("[STEP3]", "charSlice1", string(charSlice1))
	fmt.Println("[STEP3]", "charSlice2", string(charSlice2))

	// STEP4: append a '-' to charSlice2
	fmt.Println("\n[STEP4] Append '-' to charSlice2")
	charSlice2 = append(charSlice2, '-')

	fmt.Println("\n[STEP4] Result:")
	fmt.Println("[STEP4]", "charSlice1", string(charSlice1))
	fmt.Println("[STEP4]", "charSlice2", string(charSlice2))
}

However, this time, running it doesn't show what Wile was expecting:

[STEP1] Create a slice containing all the decimal digits (1,2,3,4,5,6,7,8,9,0)

[STEP2] Create a slice containing all digits from the first slice but the last one

[STEP2] Result:
[STEP2] charSlice1 1234567890
[STEP2] charSlice2 123456789

[STEP3] Changing the numbers 3 and 4 to the 'X' character in charSlice2

[STEP3] Result:
[STEP3] charSlice1 12XX567890
[STEP3] charSlice2 12XX56789

[STEP4] Append '-' to charSlice2

[STEP4] Result:
[STEP4] charSlice1 12XX56789-
[STEP4] charSlice2 12XX56789-

"In Step 4, why does appending a character in charSlice2 change the last character of charSlice1?" Wile wonders. "Weird..."

He is a bit confused, so he tries to add a further step (Step 5) that adds three more characters to charSlice2.

He writes the following code he wrote:

package main

import "fmt"

func main() {
	// STEP1: create the first slice with the digit from 1 to 0
	fmt.Println("[STEP1] Create a slice containing all the decimal digits (1,2,3,4,5,6,7,8,9,0)")
	charSlice1 := []byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}

	// STEP2: create a new slice with all the digits from slice 1 but the last one (the 0)
	fmt.Println("\n[STEP2] Create a slice containing all digits from the first slice but the last one")
	charSlice2 := charSlice1[:len(charSlice1)-1]

	fmt.Println("\n[STEP2] Result:")
	fmt.Println("[STEP2]", "charSlice1", string(charSlice1))
	fmt.Println("[STEP2]", "charSlice2", string(charSlice2))

	// STEP3: change the numbers 3 and 4 with an X in charSlice2
	fmt.Println("\n[STEP3] Changing the numbers 3 and 4 to the 'X' character in charSlice2")
	charSlice2[2] = 'X'
	charSlice2[3] = 'X'

	fmt.Println("\n[STEP3] Result:")
	fmt.Println("[STEP3]", "charSlice1", string(charSlice1))
	fmt.Println("[STEP3]", "charSlice2", string(charSlice2))

	// STEP4: append a '-' to charSlice2
	fmt.Println("\n[STEP4] Append '-' to charSlice2")
	charSlice2 = append(charSlice2, '-')

	fmt.Println("\n[STEP4] Result:")
	fmt.Println("[STEP4]", "charSlice1", string(charSlice1))
	fmt.Println("[STEP4]", "charSlice2", string(charSlice2))

	// STEP5: append '+*/' to charSlice2
	fmt.Println("\n[STEP5] Append '+*/' to charSlice2")
	charSlice2 = append(charSlice2, '+', '*', '/')

	fmt.Println("\n[STEP5] Result:")
	fmt.Println("[STEP5]", "charSlice1", string(charSlice1))
	fmt.Println("[STEP5]", "charSlice2", string(charSlice2))

}

Another surprise hits Wile. Now the output is:

[STEP1] Create a slice containing all the decimal digits (1,2,3,4,5,6,7,8,9,0)

[STEP2] Create a slice containing all digits from the first slice but the last one

[STEP2] Result:
[STEP2] charSlice1 1234567890
[STEP2] charSlice2 123456789

[STEP3] Changing the numbers 3 and 4 to the 'X' character in charSlice2

[STEP3] Result:
[STEP3] charSlice1 12XX567890
[STEP3] charSlice2 12XX56789

[STEP4] Append '-' to charSlice2

[STEP4] Result:
[STEP4] charSlice1 12XX56789-
[STEP4] charSlice2 12XX56789-

[STEP5] Append '+*/' to charSlice2

[STEP5] Result:
[STEP5] charSlice1 12XX56789-
[STEP5] charSlice2 12XX56789-+*/

Wile: "That can't be! In Step 5, it didn't change charSlice1! How can it be that the same function, called two times, has two different behaviors?"

He then decides to add another step (Step 6) where he will change back the X characters of the charSlice2 and produces the following code:

package main

import "fmt"

func main() {
	// STEP1: create the first slice with the digit from 1 to 0
	fmt.Println("[STEP1] Create a slice containing all the decimal digits (1,2,3,4,5,6,7,8,9,0)")
	charSlice1 := []byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}

	// STEP2: create a new slice with all the digits from slice 1 but the last one (the 0)
	fmt.Println("\n[STEP2] Create a slice containing all digits from the first slice but the last one")
	charSlice2 := charSlice1[:len(charSlice1)-1]

	fmt.Println("\n[STEP2] Result:")
	fmt.Println("[STEP2]", "charSlice1", string(charSlice1))
	fmt.Println("[STEP2]", "charSlice2", string(charSlice2))

	// STEP3: change the numbers 3 and 4 with an X in charSlice2
	fmt.Println("\n[STEP3] Changing the numbers 3 and 4 to the 'X' character in charSlice2")
	charSlice2[2] = 'X'
	charSlice2[3] = 'X'

	fmt.Println("\n[STEP3] Result:")
	fmt.Println("[STEP3]", "charSlice1", string(charSlice1))
	fmt.Println("[STEP3]", "charSlice2", string(charSlice2))

	// STEP4: append a '-' to charSlice2
	fmt.Println("\n[STEP4] Append '-' to charSlice2")
	charSlice2 = append(charSlice2, '-')

	fmt.Println("\n[STEP4] Result:")
	fmt.Println("[STEP4]", "charSlice1", string(charSlice1))
	fmt.Println("[STEP4]", "charSlice2", string(charSlice2))

	// STEP5: append '+*/' to charSlice2
	fmt.Println("\n[STEP5] Append '+*/' to charSlice2")
	charSlice2 = append(charSlice2, '+', '*', '/')

	fmt.Println("\n[STEP5] Result:")
	fmt.Println("[STEP5]", "charSlice1", string(charSlice1))
	fmt.Println("[STEP5]", "charSlice2", string(charSlice2))

	// STEP6: change back the X characters of charSlice2
	fmt.Println("\n[STEP6] replace the X characters with '2' and '3'")
	charSlice2[2] = '3'
	charSlice2[3] = '4'

	fmt.Println("\n[STEP6] Result:")
	fmt.Println("[STEP6]", "charSlice1", string(charSlice1))
	fmt.Println("[STEP6]", "charSlice2", string(charSlice2))
}

But again, the output is not what he was expecting:

[STEP1] Create a slice containing all the decimal digits (1,2,3,4,5,6,7,8,9,0)

[STEP2] Create a slice containing all digits from the first slice but the last one

[STEP2] Result:
[STEP2] charSlice1 1234567890
[STEP2] charSlice2 123456789

[STEP3] Changing the numbers 3 and 4 to the 'X' character in charSlice2

[STEP3] Result:
[STEP3] charSlice1 12XX567890
[STEP3] charSlice2 12XX56789

[STEP4] Append '-' to charSlice2

[STEP4] Result:
[STEP4] charSlice1 12XX56789-
[STEP4] charSlice2 12XX56789-

[STEP5] Append '+*/' to charSlice2

[STEP5] Result:
[STEP5] charSlice1 12XX56789-
[STEP5] charSlice2 12XX56789-+*/

[STEP6] replace the X characters with '2' and '3'

[STEP6] Result:
[STEP6] charSlice1 12XX56789-
[STEP6] charSlice2 123456789-+*/

Wile analyzes what happened:

  1. In Step 3, changing charSlice2 changed charSlice1 too, as expected.
  2. In Step 4, appending a character to charSlice2 changed the last character of charSlice1.
  3. in Step 5, appending 3 characters to charSlice2 didn't change charSlice1.
  4. in Step 6, changing charSlice2 didn't change charSlice1.

Wile: "What is going on? Is this a bug?"

Explanation

I can understand why Wile is confused, but the behavior is expected. He's missing some knowledge of how slices work with append function.

Let's look at the steps of his code one by one.

Step 1

charSlice1 := []byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}

This code is straightforward but does not exactly do what Wile was expecting: charSlice1 is not a 10-byte array.

Instead, charSlice1 is a view on ten bytes of a ten bytes backing byte array. Its definition is as follows:

type SliceHeader struct {
    Data uintptr
    Len int
    Cap int
}

In that structure:

  • Data is a pointer to the backing array
  • Len is the length of the slice
  • Cap is the size (capacity) of the backing array

That means that, internally, charSlice1 will have these values:

  • Data: pointer to a 10 bytes array
  • Len: 10
  • Cap: 10

Graphically, the situation looks as below:

                               charSlice1 (len: 10, cap: 10)
                                    |
                |---------------------------------------|
                |                                       |
backing_array:  | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 |

Step 2

charSlice2 := charSlice1[:len(charSlice1)-1]

Again, this is different from what Wile expects it to do. It isn't copying the array.

That assignment creates a new slice pointing to the same backing array as charSlice1, but reducing the slice length by 1.

The values of the SliceHeader for charSlice2 will be:

  • Data: pointer to the same backing array as charSlice1
  • Len: 9
  • Cap: 10

Since Len is 9, fmt.Println will print only the first nine characters of the array:

                               charSlice1 (len: 10, cap: 10)
                                    |
                |---------------------------------------|
                |                                       |
backing_array:  | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 |
                |                                   |
                |-----------------------------------|
                                  |
                             charSlice2 (len: 9, cap: 10)

Step 3

charSlice2[2] = 'X'
charSlice2[3] = 'X'

In this code, we are changing the third and the fourth characters of charSlice2. However, since the backing array for charSlice1 and charSlice2 is the same, charSlice1 will be changed, too.

​
                               charSlice1 (len: 10, cap: 10)
                                    |
                |---------------------------------------|
                |                                       |
backing_array:  | 1 | 2 | X | X | 5 | 6 | 7 | 8 | 9 | 0 |
                |                                   |
                |-----------------------------------|
                                  |
                             charSlice2 (len: 9, cap: 10)

​

Step 4

charSlice2 = append(charSlice2, '-')

Here the exciting part starts: appending a character to charSlice2 change the last character of charSlice1. Why?

Let's see what we have in memory at this point:

                               charSlice1 (len: 10, cap: 10)
                                    |
                |---------------------------------------|
                |                                       |
backing_array:  | 1 | 2 | X | X | 5 | 6 | 7 | 8 | 9 | 0 |
                |                                   |
                |-----------------------------------|
                                  |
                             charSlice2 (len: 9, cap: 10)

As you can see, both charSlice1 and charSlice2 refer to the same backing array. The only difference is that charSlice2 has a length of 9.

When we append a new character to charSlice2, the append function will increase the charSlice2 length and set its last character (the tenth byte of the backing array) with the value to append. Since charSlice1 points to the same backing array as charSlice2, changing the backing array of one slice will also affect the other.

This is how the situation appears now:

                               charSlice1 (len: 10, cap: 10)
                                    |
                |---------------------------------------|
                |                                       |
backing_array:  | 1 | 2 | X | X | 5 | 6 | 7 | 8 | 9 | - |
                |                                       |
                |---------------------------------------|
                                    |
                               charSlice2 (len: 10, cap: 10)

Step 5

charSlice2 = append(charSlice2, '+', '*', '/')

In Step 5, we saw that appending three characters to charSlice2 doesn't change charSlice1 anymore. Let's see why.

In Step 4, we increased the size of charSlice2, so now we have this situation:

                               charSlice1 (len: 10, cap: 10)
                                    |
                |---------------------------------------|
                |                                       |
backing_array:  | 1 | 2 | X | X | 5 | 6 | 7 | 8 | 9 | - |
                |                                       |
                |---------------------------------------|
                                    |
                               charSlice2 (len: 10, cap: 10)

What happens is that when we try to append the three characters to charSlice2, we exceed the capacity of the backing array. To accommodate the new data, append creates a bigger backing array, copies the data from charSlice2 to the new array, and returns a fresh slice pointing to the new backing array.

That means that from now on, charSlice1 is a stale slice and won't be in sync with charSlice2 anymore (since they now refer to 2 different backing arrays).

The new situation is now like this:

                               charSlice1 (len: 10, cap: 10)
                                    |
                |---------------------------------------|
                |                                       |
backing_array:  | 1 | 2 | X | X | 5 | 6 | 7 | 8 | 9 | - |
                |                                       |
                |---------------------------------------|


                               charSlice2 (len: 13, cap: 20)
                                    |
                |-------------------------------------------------------------------------------|
                |                                                                               |
backing_array:  | 1 | 2 | X | X | 5 | 6 | 7 | 8 | 9 | - | + | * | / |   |   |   |   |   |   |   |
                |                                                                               |
                |-------------------------------------------------------------------------------|

Step 6

charSlice2[2] = '3'
charSlice2[3] = '4'

In Step 6, we observe that changing charSlice2 doesn't change charSlice1 anymore.
After reading the previous paragraph, it should be clear why that happens: charSlice2 now points to a different backing array and has no links anymore with charSlice1.

This is how the two slices appear after the assignment:

                               charSlice1 (len: 10, cap: 10)
                                    |
                |---------------------------------------|
                |                                       |
backing_array:  | 1 | 2 | X | X | 5 | 6 | 7 | 8 | 9 | - |
                |                                       |
                |---------------------------------------|


                               charSlice2 (len: 13, cap: 20)
                                    |
                |-------------------------------------------------------------------------------|
                |                                                                               |
backing_array:  | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | - | + | * | / |   |   |   |   |   |   |   |
                |                                                                               |
                |-------------------------------------------------------------------------------|

Conclusion

The append function will try to fit the new data into the current backing array. However, if we exceed the current capacity, append will create a new backing array. That means that when working with different slices generated from the same backing array, we have to pay particular attention when we use it since it could modify other slices or diverge totally.