struct.pack gives different results when calling it again after reassigning one of its inputs

Question:

I am writing a Python script to send ICMP packets by following an example from a Japanese website.
There is part of the code where struct.pack is used to pack series of variables into a bytes array.
It looks somehow like the following:

import struct

def make_icmp_echo_request():
    _type = 8
    code = 0
    check = 0
    _id = 1
    seq = 1

    packed = struct.pack("!BBHHH", _type, code, check, _id, seq)
    return packed

make_icmp_echo_request()

No matter how much you call make_icmp_echo_request, the output will be the same.
However, the website also implements a checksum function which will be used after the ICMP packet is packed.
The weird thing is that the implementation from the website will call struct.pack one more time after the call to checksum.
Something like below:

import struct

def checksum(data):
    data_len = len(data) // 2 * 2
    csum = 0
    for i in range(0, data_len, 2):
        csum += (data[i + 1] << 8) + data[i]
    if len(data) % 2 != 0:
        csum += data[-1]
    while csum >> 16:
        csum = (csum >> 16) + (csum & 0xffff)
    csum = csum >> 8 | (csum << 8 & 0xff00)
    return ~csum&0xffff

def make_icmp_echo_request():
    _type = 8
    code = 0
    check = 0
    _id = 1
    seq = 1

    packed = struct.pack("!BBHHH", _type, code, check, _id, seq)
    check = checksum(packed)
    return struct.pack("!BBHHH", _type, code, check, _id, seq)

make_icmp_echo_request()

I found out that the output of the first call to struct.pack is different from the second one.
And this only happens if checksum is being called after the first call.

I confirmed this with below code (I also unpacked back the packed byte array to make things clearer).

import struct

def checksum(data):
    data_len = len(data) // 2 * 2
    csum = 0
    for i in range(0, data_len, 2):
        csum += (data[i + 1] << 8) + data[i]
    if len(data) % 2 != 0:
        csum += data[-1]
    while csum >> 16:
        csum = (csum >> 16) + (csum & 0xffff)
    csum = csum >> 8 | (csum << 8 & 0xff00)
    return ~csum&0xffff


def make_icmp_echo_request():
    _type = 8
    code = 0
    check = 0
    _id = 1
    seq = 1

    for i in range(10):
        print(f"[{i}]")
        packed = struct.pack("!BBHHH", _type, code, check, _id, seq)
        check = checksum(packed)
        print(packed, check)
        unpacked = struct.unpack("!BBHHH", packed)
        print(unpacked)

    return packed

make_icmp_echo_request()
*** Loop: 0 ***
b'x08x00x00x00x00x01x00x01' 63485
(8, 0, 0, 1, 1)
*** Loop: 1 ***
b'x08x00xf7xfdx00x01x00x01' 0
(8, 0, 63485, 1, 1)
*** Loop: 2 ***
b'x08x00x00x00x00x01x00x01' 63485
(8, 0, 0, 1, 1)
*** Loop: 3 ***
b'x08x00xf7xfdx00x01x00x01' 0
(8, 0, 63485, 1, 1)
*** Loop: 4 ***
b'x08x00x00x00x00x01x00x01' 63485
(8, 0, 0, 1, 1)
*** Loop: 5 ***
b'x08x00xf7xfdx00x01x00x01' 0
(8, 0, 63485, 1, 1)
*** Loop: 6 ***
b'x08x00x00x00x00x01x00x01' 63485
(8, 0, 0, 1, 1)
*** Loop: 7 ***
b'x08x00xf7xfdx00x01x00x01' 0
(8, 0, 63485, 1, 1)
*** Loop: 8 ***
b'x08x00x00x00x00x01x00x01' 63485
(8, 0, 0, 1, 1)
*** Loop: 9 ***
b'x08x00xf7xfdx00x01x00x01' 0
(8, 0, 63485, 1, 1)

Can anyone explain this?
It is also weird that the ICMP request will fail if I use the result of the first call to struct.pack.

Answers:

    check = 0
    ...
    packed = struct.pack("!BBHHH", _type, code, check, _id, seq)
    check = checksum(packed)
    return struct.pack("!BBHHH", _type, code, check, _id, seq)

The first call to pack() passes 0 for check. Then check is rebound to the result of the checksum() call. The second call to pack() uses this new value of check. So of course the results are different, if and only if checksum() doesn’t return 0.

Answered By: Tim Peters
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.