Issue 17566 - can use void initialization in @safe code to break out of stack
Summary: can use void initialization in @safe code to break out of stack
Status: NEW
Alias: None
Product: D
Classification: Unclassified
Component: dmd (show other issues)
Version: D2
Hardware: x86_64 Linux
: P3 normal
Assignee: No Owner
URL:
Keywords: safe
Depends on:
Blocks:
 
Reported: 2017-06-28 13:05 UTC by ag0aep6g
Modified: 2022-12-17 10:37 UTC (History)
1 user (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this issue.
Description ag0aep6g 2017-06-28 13:05:37 UTC
This is basically issue 17561 but without `Fiber`s. A fix for this is likely to also fix issue 17561, so 17561 could be considered a duplicate of this. I'm leaving it open for now, because it might be solvable by working around the more general issue somehow.

Related links:
* https://www.qualys.com/2017/06/19/stack-clash/stack-clash.txt
* https://github.com/dlang/druntime/pull/1698

Memory corrupting code:

----
import core.sys.posix.sys.mman;
import std.conv: text;

enum pageSize = 1024 * 4; // 4 KiB
enum stackSize = 1024 * 1024 * 3; // 3 MiB

void main()
{
    /* Allocate memory near the stack. */
    ubyte foo;
    auto stackTop = &foo + pageSize - cast(size_t) &foo % pageSize;
    auto stackBottom = stackTop - stackSize;
    auto sz = pageSize;
    void* dst = stackBottom - sz;
    void* p = mmap(dst, sz, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON,
        -1, 0);
    assert(p == dst, "failed to allocate page");
    
    /* Set it up with zeroes. */
    auto mem = cast(ubyte[]) p[0 .. sz];
    mem[] = 0;
    foreach (x; mem) assert(x == 0, text(x)); /* passes */

    /* Break out of the stack and wreak havoc. */
    wreak_havoc();
    
    /* Look at the mess. */
    foreach (x; mem) assert(x == 0, text(x)); /* fails; prints "13" */
}

void wreak_havoc() @safe
{
    ubyte[stackSize] x = void;
    x[0] = 13;
}
----

Like in issue 17561, the surrounding code is not @safe, but is actually safe (as far as I can tell). It's the void initialized static array that breaks safety.

In a 32-bit program it's also possible to get there with `malloc` instead of a targeted `mmap`:

----
/* WARNING: This fails quickly for me in a 32-bit Ubuntu VM, but it can
potentially consume all memory. */

import core.stdc.stdlib: malloc;
import std.conv: text;

enum pageSize = 1024 * 4; // 4 KiB
enum stackSize = 1024 * 1024 * 3; // 3 MiB

void main()
{
    ubyte foo;
    auto stackTop = &foo + pageSize - cast(size_t) &foo % pageSize;
    auto stackBottom = stackTop - stackSize;
    assert(cast(size_t) stackBottom % pageSize == 0);
    
    while (true)
    {
        /* Allocate memory. */
        auto sz = 1024 * 1024; // 1 MiB
        auto p = malloc(sz);
        assert(p !is null, "malloc failed");
        assert(stackBottom > p);
        
        /* See if it's near the stack. */
        size_t distance = stackBottom - p;
        if (distance <= sz)
        {
            /* Set it up with zeroes. */
            auto mem = cast(ubyte[]) p[0 .. sz];
            mem[] = 0;
            foreach (x; mem) assert(x == 0, text(x)); /* passes */
            
            /* Break out of the stack and wreak havoc. */
            wreak_havoc();
            
            /* Look at the mess. */
            foreach (x; mem) assert(x == 0, text(x)); /* fails; prints "13" */
            
            break;
        }
    }
}

void wreak_havoc() @safe
{
    ubyte[stackSize] x = void;
    x[0] = 13;
}
----
Comment 1 Walter Bright 2021-06-25 20:45:10 UTC
The compiler should reject any stack frame that's larger than 4K. This is because the operating system puts a guard page at the end of the reserved stack area, and a seg fault in that region is caught by the OS and the reserved stack area is increased.

But, if the access occurs beyond 4k, this doesn't happen. Worse, because of stack arithmetic wraparound, any address becomes accessible.