Files
raven/native/nt/seh.cpp
T

593 lines
25 KiB
C++

#include "kernel_context.h"
#include "exceptions.h"
#include "unwind.h"
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
namespace raven {
namespace {
constexpr size_t record_size = 152;
constexpr uint32_t flag_noncontinuable = 1, flag_unwinding = 2, flag_exit_unwind = 4, flag_target_unwind = 0x20, flag_nested = 0x10;
constexpr uint32_t status_unwind_consolidate = 0x80000029U, status_unwind = 0xc0000027U, status_noncontinuable = 0xc0000025U, status_invalid_disposition = 0xc0000026U;
// DISPATCHER_CONTEXT is 80 bytes; the fields below are its byte offsets.
constexpr size_t dispatcher_size = 80;
uint64_t load64(const std::vector<uint8_t> &bytes, size_t at)
{
uint64_t value;
std::memcpy(&value, bytes.data() + at, 8);
return value;
}
uint32_t le32(const std::vector<uint8_t> &bytes, size_t at)
{
uint32_t value;
std::memcpy(&value, bytes.data() + at, 4);
return value;
}
void put32(std::vector<uint8_t> &bytes, size_t at, uint32_t value)
{
std::memcpy(bytes.data() + at, &value, 4);
}
void put64(std::vector<uint8_t> &bytes, size_t at, uint64_t value)
{
std::memcpy(bytes.data() + at, &value, 8);
}
// Bump allocation of guest scratch memory for records and contexts; released when the outermost dispatch ends.
class Scratch {
public:
explicit Scratch(KernelContext &context) : context(context) {}
uint64_t take(size_t size)
{
if (!base) {
const auto range = context.memory.allocate(0, capacity, 0x3000, 4);
if (range.status)
throw std::runtime_error("native exception dispatch cannot reserve guest memory");
base = range.address;
}
top = (top + 15) & ~size_t(15);
if (top + size > capacity)
throw std::runtime_error("native exception dispatch exhausted its guest memory");
const uint64_t address = base + top;
top += size;
return address;
}
size_t mark() const { return top; }
void release(size_t to) { top = to; }
private:
KernelContext &context;
uint64_t base = 0;
size_t top = 0;
static constexpr size_t capacity = 1 << 20;
};
}
class Exceptions {
public:
Exceptions(KernelContext &context, NativeUnwinder &unwinder) : context(context), unwinder(unwinder), scratch(context) {}
bool trace = std::getenv("RAVEN_TRACE_SEH") != nullptr;
uint64_t trap_gate = 0;
// Walks the stack from the raising frame; true when a handler resumed execution through `resume`.
bool dispatch(std::vector<uint8_t> record, ContextBytes origin, ContextBytes &resume)
{
const size_t mark = scratch.mark();
const uint64_t guest_record = scratch.take(record_size);
const uint64_t dispatcher = scratch.take(dispatcher_size);
const uint64_t frame_context = scratch.take(x64_context::size);
context.process.write_memory(guest_record, record);
active.push_back({origin, context.process.callback_depth(), mark});
if (trace)
{
std::fprintf(stderr, "seh dispatch begin: code=%x active=%zu depth=%zu params=%x info1=%llx info2=%llx rsp=%llx\n", le32(record, 0), active.size(),
context.process.callback_depth(), le32(record, 0x18), (unsigned long long)load64(record, 0x28), (unsigned long long)load64(record, 0x30),
(unsigned long long)context_rsp(origin));
if (le32(record, 0x18) >= 2) {
try {
const auto data = context.process.read_memory(load64(record, 0x28), 96);
std::fprintf(stderr, "seh object:");
for (const auto byte : data) std::fprintf(stderr, " %02x", byte);
std::fprintf(stderr, "\n");
} catch (const std::runtime_error &) {
}
}
}
bool handled = false;
ContextBytes current = origin;
size_t continuation_cursor = continuation_start;
// An exception raised inside a handler continues its search through the dispatcher's frames, which the host callback
// boundary stands for, into the context of the exception that the handler was running for.
size_t outer = active.size() - 1;
for (int frames = 0; frames < 1024; ++frames) {
const uint64_t pc = context_rip(current);
if (!pc)
break;
const auto function = unwinder.lookup(pc);
if (!function) {
if (pc == context.process.return_gate() && outer > 0) {
current = active[--outer].origin;
continue;
}
// A search raised from a handler that an unwind is running continues in the frames of whoever called the unwind,
// then in the contexts of the dispatches that the unwind has already crossed.
if (pc == context.process.return_gate() && continuation_cursor < continuation.size()) {
current = continuation[continuation_cursor++];
continue;
}
if (trace)
std::fprintf(stderr, "seh frame: pc=%llx has no unwind data%s\n", (unsigned long long)pc, pc == context.process.return_gate() ? " (callback boundary)" : "");
// With no outer dispatch the boundary ends the search: no frames of the guest's caller are on this stack.
if (pc == context.process.return_gate() || !readable(pc))
break;
unwinder.unwind_leaf(current);
continue;
}
ContextBytes next = current;
const auto result = unwinder.unwind(1, pc, *function, next);
if (trace)
std::fprintf(stderr, "seh frame: pc=%llx function=%x..%x unwind=%x handler=%llx\n", (unsigned long long)pc, function->begin, function->end,
function->unwind, (unsigned long long)result.handler);
if (result.handler) {
std::vector<uint8_t> frame = current;
context.process.write_memory(frame_context, frame);
std::vector<uint8_t> descriptor(dispatcher_size);
put64(descriptor, 0, pc);
put64(descriptor, 8, function->image_base);
put64(descriptor, 0x10, function->address);
put64(descriptor, 0x18, result.establisher);
put64(descriptor, 0x20, 0);
put64(descriptor, 0x28, frame_context);
put64(descriptor, 0x30, result.handler);
put64(descriptor, 0x38, result.handler_data);
put64(descriptor, 0x40, 0);
context.process.write_memory(dispatcher, descriptor);
if (trace)
std::fprintf(stderr, "seh: pc=%llx handler=%llx frame=%llx\n", (unsigned long long)pc, (unsigned long long)result.handler, (unsigned long long)result.establisher);
const uint64_t arguments[] = {guest_record, result.establisher, frame_context, dispatcher};
const auto outcome = context.call(result.handler, arguments);
if (outcome.stop != NativeStop::returned)
throw outcome;
const uint32_t disposition = uint32_t(outcome.value);
if (disposition == 0) {
if (le32(record, 4) & flag_noncontinuable)
throw std::runtime_error("native exception handler continued a noncontinuable exception");
resume = context.process.read_memory(frame_context, x64_context::size);
handled = true;
break;
}
if (disposition != 1 && disposition != 2)
throw std::runtime_error("native exception handler returned an invalid disposition");
}
current = next;
}
if (trace)
std::fprintf(stderr, "seh dispatch end: handled=%d active=%zu\n", handled, active.size());
active.pop_back();
scratch.release(mark);
return handled;
}
// Handles one exception end to end; either redirects the guest (throwing ExecutionRedirected) or ends the process.
[[noreturn]] void raise(std::vector<uint8_t> record, ContextBytes origin, const std::function<NativeResult()> &unhandled)
{
if (std::getenv("RAVEN_WATCH_EXCEPTION") && le32(record, 0x18) >= 2) {
// Diagnosis aid: report every write to the second word of the thrown object.
uint64_t object;
std::memcpy(&object, record.data() + 0x28, 8);
context.process.watch_write(object);
}
ContextBytes resume;
const size_t depth = context.process.callback_depth();
if (dispatch(record, origin, resume))
redirect_to(resume, depth);
if (context.unhandled_filter) {
const size_t mark = scratch.mark();
const uint64_t guest_record = scratch.take(record_size), guest_context = scratch.take(x64_context::size), pointers = scratch.take(16);
context.process.write_memory(guest_record, record);
context.process.write_memory(guest_context, origin);
std::vector<uint8_t> block(16);
put64(block, 0, guest_record);
put64(block, 8, guest_context);
context.process.write_memory(pointers, block);
const uint64_t arguments[] = {pointers};
const auto outcome = context.call(context.unhandled_filter, arguments);
if (outcome.stop != NativeStop::returned)
throw outcome;
if (int32_t(outcome.value) == -1) {
resume = context.process.read_memory(guest_context, x64_context::size);
scratch.release(mark);
redirect_to(resume, depth);
}
scratch.release(mark);
}
const uint32_t code = le32(record, 0);
const auto *module = unwinder.module_at(context_rip(origin));
std::fprintf(stderr, "raven: unhandled exception %08x at %llx (%s+%llx)\n", code, (unsigned long long)context_rip(origin),
module ? module->name.c_str() : "unknown", module ? (unsigned long long)(context_rip(origin) - module->loaded.base()) : 0ULL);
throw unhandled();
}
// Execution resumes at the callback nesting where the exception was raised; handler calls above it are abandoned.
[[noreturn]] void redirect_to(const ContextBytes &target, size_t depth, size_t mark = SIZE_MAX)
{
auto native = context.process.context();
apply_context(native, target);
context.process.redirect(native, depth);
if (const char *count = std::getenv("RAVEN_STEP_REDIRECT")) {
// Diagnosis aid: log the instructions executed right after execution is redirected.
static int redirects = 0;
if (++redirects == (std::getenv("RAVEN_STEP_NTH") ? std::atoi(std::getenv("RAVEN_STEP_NTH")) : 1))
context.process.trace_instructions(size_t(std::atoi(count)), [this](uint64_t rip) {
const auto *module = unwinder.module_at(rip);
std::fprintf(stderr, "step %s+%llx\n", module ? module->name.c_str() : "?", module ? (unsigned long long)(rip - module->loaded.base()) : (unsigned long long)rip);
});
}
// Guest memory of the dispatches that this jump abandons can be reused; outer dispatches keep theirs.
if (mark != SIZE_MAX)
scratch.release(mark);
throw ExecutionRedirected{depth};
}
// The caller's context at a trapped API call: the return address on top of the stack is the next instruction.
ContextBytes caller_context()
{
auto native = context.process.context();
auto bytes = context_from_process(native);
const uint64_t rsp = context_rsp(bytes);
const auto top = context.process.read_memory(rsp, 8);
uint64_t rip;
std::memcpy(&rip, top.data(), 8);
set_context_rip(bytes, rip);
set_context_rsp(bytes, rsp + 8);
return bytes;
}
std::vector<uint8_t> new_record(uint32_t code, uint32_t flags, uint64_t address, std::span<const uint64_t> parameters)
{
std::vector<uint8_t> record(record_size);
put32(record, 0, code);
put32(record, 4, flags);
put64(record, 0x10, address);
put32(record, 0x18, uint32_t(parameters.size()));
for (size_t index = 0; index < parameters.size(); ++index)
put64(record, 0x20 + index * 8, parameters[index]);
return record;
}
// Second pass: runs termination handlers frame by frame up to the target, then resumes there.
[[noreturn]] void unwind(uint64_t target_frame, uint64_t target_ip, std::vector<uint8_t> record, uint64_t return_value)
{
put32(record, 4, le32(record, 4) | flag_unwinding | (target_frame ? 0 : flag_exit_unwind));
// Crossing a host callback boundary consumes the dispatch it belongs to; the last one consumed says where execution resumes.
size_t depth = context.process.callback_depth();
const size_t mark = scratch.mark();
size_t resume_mark = mark;
const uint64_t guest_record = scratch.take(record_size);
const uint64_t dispatcher = scratch.take(dispatcher_size);
const uint64_t frame_context = scratch.take(x64_context::size);
ContextBytes current = caller_context();
const size_t previous_start = continuation_start, previous_size = continuation.size();
size_t outer_cursor = previous_start;
continuation_start = continuation.size();
continuation.push_back(current);
struct Release {
std::vector<ContextBytes> &stack;
size_t &start, previous_start, previous_size;
~Release() { stack.resize(previous_size); start = previous_start; }
} release{continuation, continuation_start, previous_start, previous_size};
bool reached = false;
for (int frames = 0; frames < 1024 && !reached; ++frames) {
const uint64_t pc = context_rip(current);
if (pc == context.process.return_gate() && !active.empty()) {
// The host callback boundary stands in for the dispatcher's own frames: continue from where the exception was raised.
current = active.back().origin;
continuation.push_back(current);
depth = active.back().depth;
resume_mark = std::min(resume_mark, active.back().mark);
active.pop_back();
continue;
}
// With no dispatch left to cross, an unwind raised from inside another unwind continues through the contexts
// that the enclosing unwind recorded.
if (pc == context.process.return_gate() && outer_cursor < previous_size) {
current = continuation[outer_cursor++];
continue;
}
if (!pc)
break;
const auto function = unwinder.lookup(pc);
if (!function) {
unwinder.unwind_leaf(current);
continue;
}
ContextBytes next = current;
const auto result = unwinder.unwind(2, pc, *function, next);
const bool at_target = target_frame && result.establisher == target_frame;
if (trace)
std::fprintf(stderr, "seh unwind frame: pc=%llx fn=%x..%x establisher=%llx rsp=%llx->%llx handler=%llx%s\n", (unsigned long long)pc, function->begin,
function->end, (unsigned long long)result.establisher, (unsigned long long)context_rsp(current), (unsigned long long)context_rsp(next),
(unsigned long long)result.handler, at_target ? " TARGET" : "");
if (target_frame && result.establisher > target_frame && !at_target) {
std::fprintf(stderr, "raven: unwind passed its target frame\n");
context.process.abandon_call();
throw ProcessExit{status_invalid_disposition, pc};
}
if (result.handler) {
put32(record, 4, (le32(record, 4) & ~flag_target_unwind) | (at_target ? flag_target_unwind : 0));
context.process.write_memory(guest_record, record);
context.process.write_memory(frame_context, current);
std::vector<uint8_t> descriptor(dispatcher_size);
put64(descriptor, 0, pc);
put64(descriptor, 8, function->image_base);
put64(descriptor, 0x10, function->address);
put64(descriptor, 0x18, result.establisher);
put64(descriptor, 0x20, target_ip);
put64(descriptor, 0x28, frame_context);
put64(descriptor, 0x30, result.handler);
put64(descriptor, 0x38, result.handler_data);
context.process.write_memory(dispatcher, descriptor);
if (trace)
std::fprintf(stderr, "seh unwind: pc=%llx handler=%llx frame=%llx%s\n", (unsigned long long)pc, (unsigned long long)result.handler,
(unsigned long long)result.establisher, at_target ? " target" : "");
const uint64_t arguments[] = {guest_record, result.establisher, frame_context, dispatcher};
const auto outcome = context.call(result.handler, arguments);
if (outcome.stop != NativeStop::returned)
throw outcome;
// The handler of the target frame owns the context that execution resumes from, and C++ frame handlers
// leave the continuation address of the catch block they ran in the dispatcher's TargetIp.
if (at_target || uint32_t(outcome.value) == 3)
current = context.process.read_memory(frame_context, x64_context::size);
if (at_target)
target_ip = load64(context.process.read_memory(dispatcher, dispatcher_size), 0x20);
}
if (at_target) {
reached = true;
break;
}
current = next;
}
if (!reached && target_frame) {
std::fprintf(stderr, "raven: unwind never reached its target frame\n");
context.process.abandon_call();
throw ProcessExit{status_invalid_disposition, context_rip(current)};
}
if (trace)
std::fprintf(stderr, "seh resume at %llx (context rip %llx) rsp=%llx\n", (unsigned long long)target_ip, (unsigned long long)context_rip(current),
(unsigned long long)context_rsp(current));
if (le32(record, 0) == status_unwind_consolidate && trap_gate) {
// Run the runtime's callback on the target frame's stack; its return trap resumes execution.
const uint64_t callback = load64(record, 0x20);
const uint64_t base = (context_rsp(current) - record_size - 0x40) & ~uint64_t(15);
context.process.write_memory(base, record);
const uint64_t slot = base - 0x20 - 8;
std::vector<uint8_t> gate(8);
put64(gate, 0, trap_gate);
context.process.write_memory(slot, gate);
consolidations.push_back({current, slot, depth});
auto entry = current;
set_context_rip(entry, callback);
set_context_rsp(entry, slot);
set_context_register(entry, 1, base);
redirect_to(entry, depth, resume_mark);
}
if (target_ip)
set_context_rip(current, target_ip);
set_context_register(current, 0, return_value);
redirect_to(current, depth, resume_mark);
}
struct Active {
ContextBytes origin;
size_t depth;
size_t mark;
};
std::vector<Active> active;
// The contexts a running unwind was called from and the dispatch contexts it has crossed, in the order a stack walk meets them.
std::vector<ContextBytes> continuation;
size_t continuation_start = 0;
// A C++ catch is entered through STATUS_UNWIND_CONSOLIDATE: after unwinding, the runtime's callback runs on the
// target frame's stack and returns the address where execution resumes. Each pending callback remembers the frame it belongs to.
struct Consolidation {
ContextBytes target;
uint64_t return_slot;
size_t depth;
};
std::vector<Consolidation> consolidations;
[[noreturn]] void finish_consolidation(uint64_t continuation)
{
const uint64_t slot = context_rsp(context_from_process(context.process.context())) - 8;
const auto found = std::find_if(consolidations.begin(), consolidations.end(), [slot](const auto &item) { return item.return_slot == slot; });
if (found == consolidations.end())
throw std::runtime_error("native unwind consolidation returned without a pending catch");
auto target = found->target;
const size_t depth = found->depth;
// Callbacks that never returned (a rethrow left them) are abandoned together with this one.
consolidations.erase(found, consolidations.end());
set_context_rip(target, continuation);
redirect_to(target, depth);
}
private:
KernelContext &context;
NativeUnwinder &unwinder;
Scratch scratch;
bool readable(uint64_t address) const
{
try {
context.process.read_memory(address, 1);
return true;
} catch (const std::runtime_error &) {
return false;
}
}
};
void register_exception_api(ApiRegistry &registry, KernelContext &context, NativeUnwinder &unwinder)
{
auto exceptions = std::make_shared<Exceptions>(context, unwinder);
context.raise_fault = [exceptions, &context](const NativeResult &fault) {
auto native = context.process.context();
auto origin = context_from_process(native);
const uint64_t rip = context_rip(origin);
context.process.enter_fault_dispatch();
const auto code = context.process.read_memory(rip, 2);
if (fault.signal == SIGSEGV && code[0] == 0xcd && code[1] == 0x29) {
std::fprintf(stderr, "raven: fast fail %llu\n", (unsigned long long)context_register(origin, 1));
context.process.abandon_call();
throw ProcessExit{0xc0000409U, rip};
}
uint32_t status = 0xc0000005U;
std::vector<uint64_t> parameters;
switch (fault.signal) {
case SIGSEGV: parameters = {0, fault.fault_address}; break;
case SIGBUS: status = 0xc0000006U; parameters = {0, fault.fault_address, 0xc0000006U}; break;
case SIGFPE:
status = fault.signal_code == FPE_INTDIV ? 0xc0000094U : fault.signal_code == FPE_INTOVF ? 0xc0000095U :
fault.signal_code == FPE_FLTDIV ? 0xc000008eU : fault.signal_code == FPE_FLTOVF ? 0xc0000091U : 0xc000008fU;
break;
case SIGILL: status = 0xc000001dU; break;
case SIGTRAP: status = 0x80000003U; break;
default: break;
}
auto record = exceptions->new_record(status, 0, rip, parameters);
exceptions->raise(std::move(record), std::move(origin), [fault] { return fault; });
};
context.describe_stack = [&context, &unwinder] {
std::string text;
try {
auto bytes = context_from_process(context.process.context());
// Skip the API stub: the first frame is the guest code that called it.
unwinder.unwind_leaf(bytes);
for (int frame = 0; frame < 24; ++frame) {
const uint64_t pc = context_rip(bytes);
const auto *module = unwinder.module_at(pc);
if (!module)
break;
char buffer[96];
std::snprintf(buffer, sizeof buffer, " %s+%llx", module->name.c_str(), (unsigned long long)(pc - module->loaded.base()));
text += buffer;
const auto function = unwinder.lookup(pc);
if (!function)
unwinder.unwind_leaf(bytes);
else
unwinder.unwind(0, pc, *function, bytes);
}
} catch (const std::exception &) {
}
return text;
};
registry.add("RtlCaptureContext", 1, [exceptions, &context](ApiCall &call) {
call.write(call[0], exceptions->caller_context());
});
registry.add("RtlLookupFunctionEntry", 3, [&unwinder](ApiCall &call) {
const auto function = unwinder.lookup(call[0]);
if (call[1]) call.store<uint64_t>(call[1], function ? function->image_base : 0);
call.ret(function ? function->address : 0);
});
registry.add("RtlPcToFileHeader", 2, [&unwinder](ApiCall &call) {
const auto *module = unwinder.module_at(call[0]);
const uint64_t base = module ? module->loaded.base() : 0;
if (call[1]) call.store<uint64_t>(call[1], base);
call.ret(base);
});
registry.add("RtlVirtualUnwind", 8, [&unwinder](ApiCall &call) {
const auto entry = call.read(call[3], 12);
FunctionEntry function;
function.address = call[3];
std::memcpy(&function.begin, entry.data(), 4);
std::memcpy(&function.end, entry.data() + 4, 4);
std::memcpy(&function.unwind, entry.data() + 8, 4);
function.image_base = call[1];
function.module = unwinder.module_at(call[1]);
if (!function.module) {
call.fail(87);
return;
}
auto bytes = call.read(call[4], x64_context::size);
const auto result = unwinder.unwind(call.u32(0), call[2], function, bytes);
call.write(call[4], bytes);
if (call[5]) call.store<uint64_t>(call[5], result.handler_data);
if (call[6]) call.store<uint64_t>(call[6], result.establisher);
call.ret(result.handler);
});
registry.add("RaiseException", 4, [exceptions, &context](ApiCall &call) {
const uint32_t count = std::min<uint32_t>(call.u32(2), 15);
std::vector<uint64_t> parameters(count);
for (uint32_t index = 0; call[3] && index < count; ++index)
parameters[index] = call.load<uint64_t>(call[3] + index * 8ULL);
auto origin = exceptions->caller_context();
auto record = exceptions->new_record(call.u32(0), call.u32(1) & flag_noncontinuable, context_rip(origin), parameters);
exceptions->raise(std::move(record), std::move(origin), [&context] { return native_raise_exception(context.process); });
});
registry.add("RtlRaiseException", 1, [exceptions, &context](ApiCall &call) {
auto origin = exceptions->caller_context();
auto record = call.read(call[0], record_size);
put64(record, 0x10, context_rip(origin));
exceptions->raise(std::move(record), std::move(origin), [&context] { return native_raise_rtl_exception(context.process, false); });
});
registry.add("RtlUnwindEx", 6, [exceptions, &context](ApiCall &call) {
if (std::getenv("RAVEN_TRACE_SEH") && call[2]) {
// Show the thrown object as the unwinder sees it: it must still be intact here.
const auto bytes = call.read(call[2], record_size);
const uint64_t object = load64(bytes, 0x28);
try {
const auto data = call.read(object, 24);
std::fprintf(stderr, "seh unwind object %llx:", (unsigned long long)object);
for (const auto byte : data) std::fprintf(stderr, " %02x", byte);
std::fprintf(stderr, "\n");
} catch (const ApiFault &) {
}
}
(void)context;
std::vector<uint8_t> record;
if (call[2])
record = call.read(call[2], record_size);
else
record = exceptions->new_record(status_unwind, 0, 0, {});
exceptions->unwind(call[0], call[1], std::move(record), call[3]);
});
registry.add("RtlUnwind", 4, [exceptions](ApiCall &call) {
std::vector<uint8_t> record;
if (call[2])
record = call.read(call[2], record_size);
else
record = exceptions->new_record(status_unwind, 0, 0, {});
exceptions->unwind(call[0], call[1], std::move(record), call[3]);
});
const auto restore = [exceptions, &context](ApiCall &call) {
exceptions->redirect_to(call.read(call[0], x64_context::size), context.process.callback_depth());
};
exceptions->trap_gate = 0;
try {
exceptions->trap_gate = context.modules.resolve({"kernel32.dll", "RcConsolidateFrames", 0, false, 0});
} catch (const std::runtime_error &) {
}
registry.add("RcConsolidateFrames", 0, [exceptions, &context](ApiCall &call) {
(void)call;
exceptions->finish_consolidation(context.process.context().registers.rax);
});
registry.add("RtlRestoreContext", 2, restore);
registry.add("NtContinue", 2, restore);
registry.add("UnhandledExceptionFilter", 1, [](ApiCall &call) { call.ret(0); });
registry.add("RtlAddFunctionTable", 3, [](ApiCall &call) { call.ret(1); });
registry.add("RtlDeleteFunctionTable", 1, [](ApiCall &call) { call.ret(1); });
registry.add("RtlInstallFunctionTableCallback", 6, [](ApiCall &call) { call.ret(1); });
(void)status_noncontinuable;
(void)flag_nested;
}
}