Show all solutions

Test 2

Notes

Assume a Linux operating system running on the x86-64 architecture unless otherwise stated. On this architecture, sizeof(int) == 4, sizeof(long) == sizeof(void*) == 8, and the maximum alignment required by any type is 16.

Some figures and code samples can be pinned to your browser so you can keep them visible as you scroll through the test. Push the 📌 and then drag the pinned figure where you want it.

Some questions concern Problem Set 1’s m61 allocators and tests; an m61 test is a complete program that indicates success by exiting with status 0 and failure by terminating abnormally (e.g., by failing an assertion). print_object(x) prints the address of object x, followed by the hexadecimal values of its bytes in increasing address order, 16 bytes per line. print_bytes(ptr, sz) prints in a similar format the sz bytes of memory starting at ptr.

1. Bytes of a record (15 points)

This program was compiled and run on x86-64 Linux.

struct record {
    short id;
    char tag;
    int count;
    const char* name;
    unsigned char flags;
};

int main() {
    const char* buf = "alpha";
    record r = { 61, 'Q', 10000, buf, 5 };
    print_object(r);
}

It printed:

7bc3'53bf'0040  3d 00 51 00 10 27 00 00  80 01 b8 4a 88 55 00 00
7bc3'53bf'0050  05 00 00 00 00 00 00 00

QUESTION 1A. Which byte offsets within r hold padding? List them all. (The first byte of r has offset 0.)

QUESTION 1B. What is sizeof(record)?

QUESTION 1C. What is the value of r.count in hexadecimal?

QUESTION 1D. These lines are added after the call to print_object(r) (though not necessarily in this order):

    print_bytes(buf, sizeof(buf));
    print_bytes(buf, strlen(buf));
    print_object(buf);
    print_object(r.name);

Assuming that print_object(r) printed the exact addresses and bytes shown above, which of these outputs might be generated by each line of code? List all that apply.

  1. 7bc3'53bf'0020 80 01 b8 4a 88 55 00 00
  2. 7bc3'53bf'0048 80 01 b8 4a 88 55 00 00
  3. 5588'4ab8'0180 61 6c 70 68 61
  4. 5588'4ab8'0180 61 6c 70 68 61 00
  5. 5588'4ab8'0180 61 6c 70 68 61 00 00 00
  6. Sanitizer error: signed integer overflow
  7. Sanitizer error: global buffer overflow

QUESTION 1E. buf is declared before r. How might its address relate to the address of r? List all that apply and explain briefly.

  1. The address of buf must be less than the address of r
  2. The address of buf must equal the address of r.name
  3. The address of buf must be greater than the address of r
  4. None of the above

QUESTION 1F. What is the minimum size of any structure with the same members as record (but not necessarily in the same order)?

QUESTION 1G. Some of these expressions always evaluate to the same numeric value. List all pairs (or greater subsets) that always have the same value.

  1. r.name + 5 - 2
  2. &r.id
  3. (long) r.count % 2
  4. (buf + strlen(buf)) - r.name
  5. &r
  6. nullptr
  7. r.flags
  8. &r.name[3]

2. Break that allocator (18 points)

Each part of this question shows a fragment of an m61 allocator with a bug. Your job is to design an m61 test that exposes that bug (but doesn’t falsely accuse any correct allocator). You may describe the test, but ideally write code: a few lines each is enough. You may assume all allocator code not shown is correct.

All the fragments share these declarations. The buffer is 8 MiB and comes from malloc, so it is 16-byte aligned; fresh space is handed out sequentially from buffer.

constexpr size_t buffer_size = 8 << 20;   // allocated size of buffer
static char* buffer;                      // next unused byte of the buffer
static char* end_buffer;                  // one past the end of the buffer
static std::map<char*, size_t> active;    // active payload -> requested size
static std::map<char*, size_t> freed;     // free block -> block size

QUESTION 2A. This m61_malloc stores a 32-byte header before each payload; it doesn’t support memory reuse yet.

void* m61_malloc(size_t sz) {
    size_t padding = sz % 16 ? 16 - sz % 16 : 0;
    size_t bsz = 32 + sz + padding;      // header plus payload plus alignment padding
    if (bsz > size_t(end_buffer - buffer)) {
        return nullptr;
    }
    char* ptr = buffer + 32;
    // ... initialize header, insert `ptr` into `active` ...
    buffer += bsz;
    return ptr;
}

Design a test that fails for this allocator. Your test should not require memory reuse.

QUESTION 2B. This m61_malloc uses purely external metadata; it doesn’t support wild-write detection yet.

void* m61_malloc(size_t sz) {
    if (sz > buffer_size) {
        return nullptr;
    }
    // ... maybe reuse a free block ...
    size_t bsz = sz % 16 ? (sz | 15) + 1 : sz;  // round up to multiple of 16
    if (bsz > size_t(end_buffer - buffer)) {
        return nullptr;
    }
    active.insert({ buffer, sz });
    char* ptr = buffer;
    buffer += sz;
    return ptr;
}

Design a test that fails for this allocator. Your test should not involve a wild write.

QUESTION 2C. This allocator detects trailing wild writes with a canary byte stored immediately after each allocation.

void* m61_malloc(size_t sz) {
    // ... check `sz`, maybe reuse a free block ...
    size_t bsz = !sz || sz % 16 ? (sz | 15) + 1 : sz;  // round up to multiple of 16
    if (bsz > size_t(end_buffer - buffer)) {
        return nullptr;
    }
    active.insert({ buffer, sz });
    char* ptr = buffer;
    buffer[sz] = 0xFE;  // canary value
    buffer += bsz;
    return ptr;
}

void m61_free(void* ptr) {
    char* cptr = reinterpret_cast<char*>(ptr);
    // ... return if !ptr, detect invalid and double frees ...
    size_t sz = active[cptr];
    assert((unsigned char) cptr[sz] == 0xFE);  // check that canary value is unchanged
    // ... free the block ...
}

Design a wild-write subtest that demonstrates a problem with this allocator. A correct allocator would report the wild write when the allocation was freed (so the subtest is expected to abort); this allocator, given the right test, will not detect the wild write (so the subtest would not abort). Hint: Our subtest starts like this:

void ww_subtest() {
    char* p = (char*) m61_malloc(16);
    char* q = (char*) m61_malloc(1);
    assert(p && q);
    p[16] = 0;          // wild write: one zero byte just past the allocation

QUESTION 2D. This allocator reuses memory. Assume m61_malloc correctly serves requests from freed blocks when possible, with correct splitting. Here is m61_free.

void m61_free(void* ptr) {
    char* cptr = reinterpret_cast<char*>(ptr);
    // ... return if !ptr, check for invalid and double frees ...
    size_t sz = active[cptr];
    size_t bsz = !sz || sz % 16 ? (sz | 15) + 1 : sz;  // round up to multiple of 16
    active.erase(cptr);  // free allocation
    auto next = freed.find(cptr + bsz);  // coalesce with following free block, if any
    if (next != freed.end()) {
        bsz += next->second;
        freed.erase(next);
    }
    freed.insert({ cptr, bsz });
}

Design a test that fails for this allocator. Your test should not involve wild writes. Hint: Our test starts like this:

int main() {
    char* a = (char*) m61_malloc(1 << 20);
    char* b = (char*) m61_malloc(1 << 20);
    char* c = (char*) m61_malloc((6 << 20) - 128);
    assert(a && b && c);

3. Could have compiled (12 points)

Each part of this question shows x86-64 assembly produced by an optimizing compiler, followed by several C++ functions. Which functions could a correct compiler have translated into that assembly? A function qualifies if the assembly behaves as the C++ abstract machine requires on every call that does not execute undefined behavior. Assume assertions are enabled.

QUESTION 3A.

<some_f>:
        xorl    %eax, %eax
        testl   %edi, %edi
        jne     .L2
        incl    %eax
.L2:
        ret

Which of these functions could compile to that assembly? List all that apply or say “none of the above”.

  1. int f1(int x) {
        return x == 0;
    }
    
    int f2(long y) {
        return y == 0;
    }
    
    int f3(int z) {
        assert(false);
    }
    
  2. int f4(int m) {
        return !m;
    }
    
    int f5(int q) {
        if (q & q == q) {
            return true;
        }
        return false;
    }
    
    long f6(unsigned u) {
        return u == 0;
    }
    
  3. int f7(int a, int b) {
        return a == 0;
    }
    

QUESTION 3B.

<some_g>:
        movl    $1, %eax
        ret

Which of these functions could compile to that assembly? List all that apply or say “none of the above”.

  1. bool g1() {
        return true;
    }
    
    int g2(int x) {
        return x == x;
    }
    
    int g3(int x) {
        return x != 0;
    }
    
    int g4(int x) {
        return x + 1 > x;
    }
    
  2. int g5(unsigned x) {
        return x + 1 > x;
    }
    
    int g6(int x) {
        return x / x == 1;
    }
    
    int awesome[4] = {0, 1, 2, 3};
    bool g7(int v) {
        for (int i = 0; i <= 4; ++i) {
            if (awesome[i] == v) {
                return true;
            }
        }
        return false;
    }
    

QUESTION 3C.

<some_h>:
        movl    (%rsi), %eax
        leal    (%rdi,%rax,4), %eax
        ret

Which of these functions could compile to that assembly? List all that apply or say “none of the above”.

  1. int h1(int a, const int* b) {
        return a + 4 * *b;
    }
    
    int* h2(int* a, const int* b) {
        return a + *b;
    }
    
  2. int h3(const int* a, int b) {
        return *a + 4 * b;
    }
    
    int h4(int a, const long* b) {
        return a + 4 * *b;
    }
    
  3. int h5(int a, const short* b) {
        return a + 4 * *b;
    }