# OP_SCALL in LHS of a OP_ASGN resulting in arbitrary memory write

mruby compiler's gen_assignment returns early for NODE_SCALL LHS, leaving a stale register that lets OP_ARYCAT write to an attacker-controlled address.

- Payout: hidden
- Severity: critical
- Program: Shopify
- Found by: avisaven
- Disclosed: 2017-05-30
- Write-up quality: 8/10
- Original: https://hackerone.com/reports/226200
- Library page: https://bugbountylibrary.com/r/h1%3A226200
- Tags: area:oss domain:oss impact:rce kind:bounty platform:hackerone sev:critical tech:ruby technique:source-review vuln:memory-corruption

**Target:** mruby compiler codegen (codegen.c gen_assignment) and VM OP_ARYCAT
**Root cause:** gen_assignment's default case uses `return;` instead of `break;`, skipping `if (val) push(val);` so the assignment value register is never pushed; subsequent opcodes consume the register below, letting the splat OP_ARYCAT destination be pointed at a fake RArray with arbitrary ptr.
**Steps:**
1. Write a script assigning to a splat/OP_SCALL LHS: `x = 0x4242422a` then `a = *(_&.__=0)` twice.
2. Compile with mruby; observe opcodes where OP_ARYCAT R1 R2 uses R1 (x) as destination instead of the pushed value.
3. Run under ASan: SEGV at address 0x000042424242 in ary_concat array.c:265, confirming the destination pointer is taken from the stale register.
4. Craft the fake RArray ptr to target arbitrary memory for a write.
**How found:** Reading gen_assignment in codegen.c, noticing the default case `return;` skips the final `push(val)`, then testing an OP_SCALL LHS with a splat to see which register OP_ARYCAT actually used.
**Exploitation:** Attacker supplies mruby source; the miscompiled OP_ARYCAT treats the stale register value as an RArray destination, so a crafted value becomes the write pointer, giving a write-what-where primitive (author was experimenting toward string corruption and code execution).
**Escalation:** Chaining the write-what-where with string corruption to achieve code execution.
**Insight:** When auditing compilers/interpreters, check that every codegen case leaves the value stack balanced; a missing push makes later opcodes silently consume a neighboring register, turning type-confusion into a write primitive via opcodes like OP_ARYCAT that trust their destination.

## Proof of concept (copied exactly from the original report)

**Ruby PoC triggering arbitrary memory write**

```
x = 0x4242422a
	a = *(_&.__=0)
	a = *(_&.__=0)
    
# Generated Opcodes
```

**ASAN crash showing write to 0x424242**

```
unknown lhs 30
    unknown lhs 30
    ASAN:SIGSEGV
    =================================================================
    ==17426==ERROR: AddressSanitizer: SEGV on unknown address 0x000042424242 (pc 0x00010e982109 bp 0x7fff5127b430 sp 0x7fff5127b350 T0)
        #0 0x10e982108 in ary_concat array.c:265
        #1 0x10e981faf in mrb_ary_concat array.c:284
        #2 0x10eb9d788 in mrb_vm_exec vm.c:2404
        #3 0x10eb71298 in mrb_vm_run vm.c:860
        #4 0x10ebaea21 in mrb_top_run vm.c:2733
        #5 0x10ecc118a in mrb_load_exec parse.y:5780
        #6 0x10ecc1ef1 in mrb_load_file_cxt parse.y:5789
        #7 0x10e97cdfc in main mruby.c:227
        #8 0x7fff8f70d5c8 in start (/usr/lib/system/libdyld.dylib+0x35c8)
        #9 0x1  (<unknown module>)

    AddressSanitizer can not provide additional info.
    SUMMARY: AddressSanitizer: SEGV array.c:265 ary_concat
```

