Code change:
<!-- Thank you for filing a bug report! Please provide a short summary of the bug, along with any information you feel relevant to replicating the bug. --Excessive (extremely excessive) chaining of operators causes a stack overflow in rustc. In my experimenting I found that this happens with precisely 6115 chained operators in the playground, but it appears to depend on your compilation environment as it took approx. 8000 on my machine. This is easily avoided by splitting the chain (or having an iota of sanity), but I would expect to at least exit gracefully in this case. The code below uses only the Not () operator, but at least the Neg () operator also works, as does mixing the two (ie ). <details<summaryCode</summary</details<details<summaryLocal (Windows) Error Message</summary</detailsThe playground error message was too long to include here, but a copy can be found in this gist: <!-- If you're using the stable version of the compiler, you should also check if the bug also exists in the beta or nightly versions. --The above code was tested in the playground (Debug, Stable, v1.68.1), and a version with 8000 chained operators crashed on my local machine with the following :
Technically, it did (in the Windows case). The stack overflow was caught by Rust's handler and a message emitted to explain that it had reached stack overflow, instead of simply exiting without remark. This fulfills the letter of your expectation for it to "exit gracefully", but perhaps not your intent. You may wish to clarify how graceful an exit you were hoping for. It may also be interesting to know if you somehow found a way to use this to cause a stack overflow that did not emit such a message. I am not sure how to interpret the Playground case, honestly, perhaps that qualifies... Possibly we can manage to avoid overflowing, in any case. I only make such a pedantic note because it can be hard to do better in the face of resource exhaustion.
That's a very fair point. I had actually done the playground one first after someone on Reddit mentioned this, and only tried on windows while writing this. What's weird is that while windows was able to at least give me a general idea of what happened, the playground straight died with a SIGSEGV.
The way the Playground one goes, I really wondered if there is something that happens due to libc??? But no, the backtrace is really thousands of lines like this:
Disregard that, I was talking nonsense and looking at the wrong values. Stack offset is only a bit over ~1KB but consistently is about ~1KB each time. I think the problem is more just "no attempt to guard the final page" or something like that.
Well, I see this note in So that might be a possible fix here (if it's not already called in there).
Diagnosing stack overflows is imperfect on Linux. It's always a at first, and then we have a signal handler that tries to decide if that fault looks like it hit the stack guard page so we can report a nicer abort. But the kernel manages the guard page in the main thread with its dynamically growing stack, so we have to try to guess where that may be. We should do better with fixed thread stacks -- but I think also messes with these heuristics.
Example now compiles with... more operators, since , but still meets its demise and that grody error. Why? Because it hits the parser: Note that the signal handler dumps that huge error message on Linux because we install a custom signal handler, which feels like it was written in the Medieval Rust Era or something. On Windows, we simply wind up using std's signal handler, which prints that simple message. So I guess to resolve this bug we should improve the error message or simply delete that code.
I have opened to make the error message less cryptic, when that merges I think we should declare victory on this.
Victory declared.

Related discussion:
text: <ins> //! Signal handler for rustc //! Primarily used to extract a backtrace from stack overflow use std::alloc::{alloc, Layout}; use std::{fmt, mem, ptr}; extern "C" { fn backtrace_symbols_fd(buffer: *const *mut libc::c_void, size: libc::c_int, fd: libc::c_int); } fn backtrace_stderr(buffer: &[*mut libc::c_void]) { let size = buffer.len().try_into().unwrap_or_default(); unsafe { backtrace_symbols_fd(buffer.as_ptr(), size, libc::STDERR_FILENO) }; } /// Unbuffered, unsynchronized writer to stderr. /// /// Only acceptable because everything will end soon anyways. struct RawStderr(()); impl fmt::Write for RawStderr { fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> { let ret = unsafe { libc::write(libc::STDERR_FILENO, s.as_ptr().cast(), s.len()) }; if ret == -1 { Err(fmt::Error) } else { Ok(()) } } } /// We don't really care how many bytes we actually get out. SIGSEGV comes for our head. /// Splash stderr with letters of our own blood to warn our friends about the monster. macro raw_errln($tokens:tt) { let _ = ::core::fmt::Write::write_fmt(&mut RawStderr(()), format_args!($tokens)); let _ = ::core::fmt::Write::write_char(&mut RawStderr(()), 'n'); } /// Signal handler installed for SIGSEGV extern "C" fn print_stack_trace(_: libc::c_int) { const MAX_FRAMES: usize = 256; // Reserve data segment so we don't have to malloc in a signal handler, which might fail // in incredibly undesirable and unexpected ways due to e.g. the allocator deadlocking static mut STACK_TRACE: [*mut libc::c_void; MAX_FRAMES] = [ptr::null_mut(); MAX_FRAMES]; let stack = unsafe { // Collect return addresses let depth = libc::backtrace(STACK_TRACE.as_mut_ptr(), MAX_FRAMES as i32); if depth == 0 { return; } &STACK_TRACE.as_slice()[0..(depth as _)] }; // Just a stack trace is cryptic. Explain what we're doing. raw_errln!("error: rustc interrupted by SIGSEGV, printing backtracen"); let mut written = 1; let mut consumed = 0; // Begin elaborating return addrs into symbols and writing them directly to stderr // Most backtraces are stack overflow, most stack overflows are from recursion // Check for cycles before writing 250 lines of the same ~5 symbols let cycled = |(runner, walker)| runner == walker; let mut cyclic = false; if let Some(period) = stack.iter().skip(1).step_by(2).zip(stack).position(cycled) { let period = period.saturating_add(1); // avoid "what if wrapped?" branches let Some(offset) = stack.iter().skip(period).zip(stack).position(cycled) else { // impossible. return; }; // Count matching trace slices, else we could miscount "biphasic cycles" // with the same period + loop entry but a different inner loop let next_cycle = stack[offset..].chunks_exact(period).skip(1); let cycles = 1 + next_cycle .zip(stack[offset..].chunks_exact(period)) .filter(|(next, prev)| next == prev) .count(); backtrace_stderr(&stack[..offset]); written += offset; consumed += offset; if cycles > 1 { raw_errln!("n### cycle encountered after {offset} frames with period {period}"); backtrace_stderr(&stack[consumed..consumed + period]); raw_errln!("### recursed {cycles} timesn"); written += period + 4; consumed += period * cycles; cyclic = true; }; } let rem = &stack[consumed..]; backtrace_stderr(rem); raw_errln!(""); written += rem.len() + 1; let random_depth = || 8 * 16; // chosen by random diceroll (2d20) if cyclic || stack.len() > random_depth() { // technically speculation, but assert it with confidence anyway. // rustc only arrived in this signal handler because bad things happened // and this message is for explaining it's not the programmer's fault raw_errln!("note: rustc unexpectedly overflowed its stack! this is a bug"); written += 1; } if stack.len() == MAX_FRAMES { raw_errln!("note: maximum backtrace depth reached, frames may have been lost"); written += 1; } raw_errln!("note: we would appreciate a report at https://github.com/rust-lang/rust"); written += 1; if written > 24 { // We probably just scrolled the earlier "we got SIGSEGV" message off the terminal raw_errln!("note: backtrace dumped due to SIGSEGV! resuming signal"); }; } /// When SIGSEGV is delivered to the process, print a stack trace and then exit. pub(super) fn install() { unsafe { let alt_stack_size: usize = min_sigstack_size() + 64 * 1024; let mut alt_stack: libc::stack_t = mem::zeroed(); alt_stack.ss_sp = alloc(Layout::from_size_align(alt_stack_size, 1).unwrap()).cast(); alt_stack.ss_size = alt_stack_size; libc::sigaltstack(&alt_stack, ptr::null_mut()); let mut sa: libc::sigaction = mem::zeroed(); sa.sa_sigaction = print_stack_trace as libc::sighandler_t; sa.sa_flags = libc::SA_NODEFER | libc::SA_RESETHAND | libc::SA_ONSTACK; libc::sigemptyset(&mut sa.sa_mask); libc::sigaction(libc::SIGSEGV, &sa, ptr::null_mut()); } } /// Modern kernels on modern hardware can have dynamic signal stack sizes. #[cfg(any(target_os = "linux", target_os = "android"))] fn min_sigstack_size() -> usize { const AT_MINSIGSTKSZ: core::ffi::c_ulong = 51; let dynamic_sigstksz = unsafe { libc::getauxval(AT_MINSIGSTKSZ) }; // If getauxval couldn't find the entry, it returns 0, // so take the higher of the "constant" and auxval. // This transparently supports older kernels which don't provide AT_MINSIGSTKSZ libc::MINSIGSTKSZ.max(dynamic_sigstksz as _) } /// Not all OS support hardware where this is needed. #[cfg(not(any(target_os = "linux", target_os = "android")))] fn min_sigstack_size() -> usize { libc::MINSIGSTKSZ } </ins>