Lokathor/bytemuck

Stack overflow when using `zeroed_box()`

Closed this issue · 4 comments

With a debug build, I'm consistently getting a stack overflow when using zeroed_box(). Minimal example:

const PAGE_SIZE: usize = 4096;
type BlockChunk = [u8; PAGE_SIZE];

const MIB_16: usize = 16 * 1024 * 1024;
const SUPER_SIZE: usize = MIB_16 / std::mem::size_of::<BlockChunk>();
type SuperPage = [BlockChunk; SUPER_SIZE];

#[test]
fn test_alloc() {
    // ABORT: thread '[...]::test_alloc' has overflowed its stack
    let _: Box<SuperPage> = bytemuck::zeroed_box();
}

EDIT: This happens on windows, have not checked anything else yet.

Reproducible on playpen: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=547372389428bf450c9bf885cf7ea6b8

As far as I can tell, zeroed_box, which is try_zeroed_box with an unwrap, can't possibly be using more than 5 or 6 usize worth of stack space itself. So the allocator must be doing something funky.

I'm putting you on the case!

So, I'm currently guessing there is undefined behavior in play. This repoduces the issue standalone:

use std::alloc;
use std::mem::{size_of, align_of, zeroed};
use std::alloc::{alloc_zeroed, Layout};

#[inline]
pub fn try_zeroed_box<T>() -> Result<Box<T>, ()> {
  if size_of::<T>() == 0 {
    return Ok(Box::new(unsafe{zeroed()}));
  }
  let layout =
    Layout::from_size_align(size_of::<T>(), align_of::<T>()).unwrap();
  let ptr = unsafe { alloc_zeroed(layout) };
  if ptr.is_null() {
    // we don't know what the error is because `alloc_zeroed` is a dumb API
    Err(())
  } else {
    Ok(unsafe { Box::<T>::from_raw(ptr as *mut T) })
  }
}

const PAGE_SIZE: usize = 4096;
type BlockChunk = [u8; PAGE_SIZE];

const MIB_16: usize = 16 * 1024 * 1024;
const SUPER_SIZE: usize = MIB_16 / std::mem::size_of::<BlockChunk>();
type SuperPage = [BlockChunk; SUPER_SIZE];

fn main() {
    // ABORT: thread '[...]::test_alloc' has overflowed its stack
    let _: Box<SuperPage> = try_zeroed_box().unwrap();
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=3622ed3090e28f58875d9932804c998a

Note in particual these lines:

if size_of::<T>() == 0 {
    return Ok(Box::new(unsafe{zeroed()}));
}

I'm guessing that this somehow is undefined behavior if expanded with a very large T

Actually its perfectly well define beheavior - its a normal branch, which contains a temporary stack variable that needs to be allocated in the stack frame. So the issue is just how to avoid it.

Fixed by #43