Skip to main content
3 of 4
-1 byte: parser abuse
EasyasPi
  • 5k
  • 17
  • 22

Assembly (MIPS, SPIM), interpreter bug, 15 14 bytes

main:sb$0 f
f:

Try it online!

Here's a cute one I found by accident.

This attempts to store a byte to the address at label f. This is in the .text section.

SPIM is pretty clever in that writing out of bounds of a section will just grow the section. However, writing to .text is tricky.

SPIM stores instructions in a struct for easier parsing it and debugging, instead of just storing data directly.

CPU/inst.h:57:

/* Representation of an instruction. Store the instruction fields in an
   overlapping manner similar to the real encoding (but not identical, to
   speed decoding in C code, as opposed to hardware).. */

typedef struct inst_s
{
  short opcode;

  union
    {
      // Snip
    } r_t;

  int32 encoding;
  imm_expr *expr;
  char *source_line;
} instruction;

Therefore, to store to .text, SPIM will give it a special treatment in bad_mem_write().

CPU/mem.cpp:395:

void
set_mem_byte(mem_addr addr, reg_word value)
{
  data_modified = true;
  // .data
  if ((addr >= DATA_BOT) && (addr < data_top))
    data_seg_b [addr - DATA_BOT] = (BYTE_TYPE) value;
  // .stack
  else if ((addr >= stack_bot) && (addr < STACK_TOP))
    stack_seg_b [addr - stack_bot] = (BYTE_TYPE) value;
  // .data
  else if ((addr >= K_DATA_BOT) && (addr < k_data_top))
    k_data_seg_b [addr - K_DATA_BOT] = (BYTE_TYPE) value;
  // .text or section out of bounds
  else
    bad_mem_write (addr, value, 0); // <--
}

In bad_mem_write(), SPIM will attempt to splice and recompile the instruction as if you modified the memory directly. CPU/mem.cpp:506:

static void
bad_mem_write (mem_addr addr, mem_word value, int mask)
{
  mem_word tmp;

  if ((addr & mask) != 0)
    /* Unaligned address fault */
    RAISE_EXCEPTION (ExcCode_AdES, CP0_BadVAddr = addr)
    else if (addr >= TEXT_BOT && addr < text_top)
  {
    // For halfword and byte writes, attempt to overwrite part of the instruction.
    switch (mask)
    {
    case 0x0:
      tmp = ENCODING (text_seg [(addr - TEXT_BOT) >> 2]);

#ifdef SPIM_BIGENDIAN
      tmp = ((tmp & ~(0xff << (8 * (3 - (addr & 0x3)))))
               | (value & 0xff) << (8 * (3 - (addr & 0x3))));
#else
      tmp = ((tmp & ~(0xff << (8 * (addr & 0x3))))
               | (value & 0xff) << (8 * (addr & 0x3)));
#endif
      break;
      // ...
    }

    // Free instruction if it isn't NULL
    if (text_seg [(addr - TEXT_BOT) >> 2] != NULL)
    {
      free_inst (text_seg[(addr - TEXT_BOT) >> 2]);
    }
    // create a new instruction with the encoding
    text_seg [(addr - TEXT_BOT) >> 2] = inst_decode (tmp);

But wait a second....

SPIM checks for NULL... CPU/mem.cpp:549:

if (text_seg [(addr - TEXT_BOT) >> 2] != NULL)

...after it already dereferenced it!

CPU/mem.cpp:519 (after macro expansion)

tmp = text_seg [(addr - TEXT_BOT) >> 2]->encoding;

And what would cause this instruction to be NULL? If it is not an instruction. And what is at the label f? Nothing, so SPIM dereferences a NULL pointer.

  • -1 byte: Parser abuse
EasyasPi
  • 5k
  • 17
  • 22