[Docs] reSTify oldwiki/Signal-Handling-in-Graphene.md

This commit is contained in:
Wojtek Porczyk
2020-03-22 00:20:21 +01:00
committed by Michał Kowalczyk
parent 3fc67d6de2
commit 613e9f7ea7
4 changed files with 785 additions and 749 deletions
+771
View File
@@ -0,0 +1,771 @@
Signal Handling in Graphene
===========================
.. highlight:: text
(Disclaimer: This explanation is partially outdated. It is intended only as an internal
reference for developers of Graphene, not as a general documentation for Graphene users.)
Signal Handling
---------------
This analysis is written while Graphene's signal handling mechanisms are in flux. In future, all
Graphene PALs should implement the same mechanism, and LibOS should adopt a better scheme to
support nested signals and alternate signal stacks.
In the interest of space and mental sanity, we do not discuss FreeBSD PAL implementation.
Historically, Linux and FreeBSD shared the same mechanism (where signals were immediately delivered
to LibOS even if signal arrived during PAL call). This old mechanism was adopted by Linux-SGX PAL,
though due to peculiarities of Intel SGX, it has its own sub-flows and is more complicated.
Currently, Linux PAL implements a new mechanism where a signal during a PAL call is pended and is
delivered to LibOS only after the PAL call is finished.
So, there are two signal-handling mechanisms at the PAL layer:
* Linux PAL: (1) If signal arrives during PAL call, pend it and return from signal context,
continuing normal context of PAL call. Immediately after a PAL call is finished, deliver all
pending signals to LibOS. (2) If signal arrives during LibOS/application code, deliver the
signal immediately to LibOS. Note that the signal delivery and handling is done in signal context
(in contrast to pending-signal delivery).
* Linux-SGX PAL: (1) If signal arrives during enclave-code execution, remember the interrupted
enclave-code context and return from signal context. When jumping back into the enclave (in normal
context), deliver the signal to LibOS. After handling the signal, LibOS/PAL will continue from
interrupted enclave-code context. (2) If signal arrives during non-enclave-code, i.e.
untrusted-PAL, execution, just return from signal context. When jumping back into the enclave
(in normal context), deliver the signal to LibOS. In contrast to first case, after handling the
signal, LibOS/PAL will continue as if outermost PAL function failed with `PAL_ERROR_INTERRUPTED`.
The advantage of the first mechanism is that there is never a possibility of nested PAL calls
(which is not supported by Graphene). However, this also disallows nested signals already at the
PAL layer. The advantage of the second mechanism is that nested signals are possible, at least as
far as it concerns the PAL layer.
There is a single unified signal-handling mechanism at the LibOS layer. This mechanism does *not*
support nested signals: if a signal is delivered while another signal is handled (or during a LibOS
internal lock), then it is pended. Pended signals are delivered after any system-call completion
or after any LibOS internal unlock.
A new signal-handling mechanism at the LibOS layer was proposed by Isaku Yamahata
(see https://github.com/oscarlab/graphene/pull/347). This proposal changes the points at which
signals are delivered to the user app. The two points are (1) if signal arrives during app
execution, the signal is delivered after host OS returns from signal context, and (2) if signal
arrives during LibOS/PAL execution, the signal is delivered after system-call completion. This is
in contrast to current LibOS approach of (1) delivering the first signal even in the middle of
emulated syscall, and (2) pending nested signals until system-call completion.
Linux-SGX PAL Flows
^^^^^^^^^^^^^^^^^^^
Initialization of Signal Handling
"""""""""""""""""""""""""""""""""
::
Normal context
+----------------+
... load_enclave() ...
+
| sgx_signal_setup()
| +
| | set_sighandler(SIGTERM | SIGINT | SIGCONT) +---------------+
| | + | async signals |
| | | action = struct sigaction( +---------------+
| | | sa_handler = _DkTerminateSighandler,
| | | sa_restorer = rt_sigreturn,
| | | sa_flags = {SA_SIGINFO | SA_RESTORER}
| | | sa_mask = {SIGCONT}
| | | )
| | |
| | | rt_sigaction(SIGTERM, action)
| | | rt_sigaction(SIGINT, action)
| | | rt_sigaction(SIGCONT, action)
| | |
| | + rt_sigprocmask(SIGUNBLOCK, SIGTERM | SIGINT | SIGCONT)
| |
| | set_sighandler(SIGSEGV | SIGILL | SIGFPE | SIGBUS) +--------------+
| | + | sync signals |
| | | action = struct sigaction( +--------------+
| | | sa_handler = _DkResumeSighandler,
| | | sa_restorer = rt_sigreturn,
| | | sa_flags = {SA_SIGINFO | SA_RESTORER}
| | | sa_mask = {SIGCONT}
| | | )
| | |
| | | rt_sigaction(SIGSEGV, action)
| | | rt_sigaction(SIGILL, action)
| | | rt_sigaction(SIGFPE, action)
| | | rt_sigaction(SIGBUS, action)
| | |
+ + + rt_sigprocmask(SIGUNBLOCK, SIGSEGV | SIGILL | SIGFPE | SIGBUS)
Async Signal Arrives During Enclave Code Execution
""""""""""""""""""""""""""""""""""""""""""""""""""
On the example of SIGINT, until we arrive into `_DkGenericSignalHandle()`.
::
Normal context Signal context
+----------------+ +----------------+
+ ... enclave code .....
|
| +-----+
| | AEX due to SIGINT
| <-----+
| signal handler called
| +----------------------------------------> _DkTerminateSighandler(SIGINT, siginfo, uc)
| +
| | sgx_raise(PAL_EVENT_SUSPEND)
| | +
| | | RDX = after_resume() addr
| | | RBX = current thread's TCS
| | | RCX = async_exit_pointer() addr
| | | RDI = PAL_EVENT_SUSPEND (from first func arg)
| | |
| | | EENTER(RBX, RCX) <---+
| | | + |
| | | | (SGX creates new SSA frame and |
| | | | sets RAX = current-SSA-frame = 1) |e
| | | | |n
| | | | enclave_entry() |c
| | | | + |l
| | | | | jump to handle_resume() |a
| | | | | +>
| | | | | <double-check RDI contains signum > |e
| | | | | |
| | | | | jump to handle_exception() |m
| | | | | |o
| | | | | create new enclave-thread stack frame |d
| | | | | and push GPRSGX registers on this frame |e
| | | | | |
| | | | | update GPRSGX = (RSP = new frame, |
| | RDI = PAL_EVENT_SUSPEND, |
| | | | | RSI = new frame, |
| | | | | RIP = _DkExceptionHandler) |
| | | | | |
| | | + + EEXIT(RDX = after_resume) <---+
| signal handler done | |
| <----------------------------------------+ + + after_resume(): return
|
| async_exit_pointer()
| +
| | ERESUME <---+
| | + |
| | | (SGX's current-SSA-frame = 0, thus |e
| | | enclave-thread's GPRSGX is loaded in regs) |n
| | | |c
| | | _DkExceptionHandler(exit_info = RDI = PAL_EVENT_SUSPEND, uc = RSI = new frame) |l
| | | + |a
| | | | PAL_CONTEXT ctx = copy(uc) <ctx contains interrupted-context frame> |v
| | | | |e
+ + + + _DkGenericSignalHandle(PAL_EVENT_SUSPEND, ctx) v
< ... >
Async Signal Arrives During Non-Enclave Code Execution
""""""""""""""""""""""""""""""""""""""""""""""""""""""
Non-enclave code execution can only happen if Graphene process is currently executing untrusted-PAL
code, e.g., is blocked on a `futex(wait)` system call.
On the example of SIGINT, until we arrive into `_DkGenericSignalHandle()`.
::
Normal context Signal context
+----------------+ +----------------+
+ ... non-enclave code ...
|
| +-----+
| | SIGINT
| <-----+
| signal handler called
| +----------------------------------------> _DkTerminateSighandler(SIGINT, siginfo, uc)
| +
| | update normal-context registers =
| | (RIP = sgx_entry_return,
| | RDI = -PAL_ERROR_INTERRUPTED,
| signal handler done | RSI = PAL_EVENT_SUSPEND)
| <----------------------------------------+ +
|
| sgx_entry_return()
| +
| | RDX = sgx_entry() addr
| | RBX = current thread's TCS
| | RCX = async_exit_pointer() addr
| |
| | EENTER(RBX, RCX) <--+
| | + |
| | | (SGX's current-SSA-frame = 0, ocall is done) |
| | | |e
| | | enclave_entry() |n
| | | + |c
| | | | jump to return_from_ocall() |l
| | | | |a
| | | | remember RDI = -PAL_ERROR_INTERRUPTED on enclave stack |v
| | | | |e
| | | | _DkHandleExternalEvent(event = RDI = PAL_EVENT_SUSPEND, |
| | | | + uc = RSI = enclave frame) |m
| | | | | |o
| | | | | frame = get_frame(uc) <finds outermost PAL function> |d
| | | | | |e
| | | | | _DkGenericSignalHandle(PAL_EVENT_SUSPEND, frame) |
< ... > v
Sync Signal Arrives During Enclave Code Execution
"""""""""""""""""""""""""""""""""""""""""""""""""
This case is exactly the same as for async signal. The only difference in the diagram would be that
`_DkTerminateSighandler` is replaced by `_DkResumeSighandler`. But the logic is exactly the same.
Sync Signal Arrives During Non-Enclave Code Execution
"""""""""""""""""""""""""""""""""""""""""""""""""""""
Non-enclave code execution can only happen if Graphene process is currently executing untrusted-PAL
code, e.g., is blocked on a `futex(wait)` system call.
If a sync signal arrives in this case, it means that there was a memory fault, illegal instruction,
or arithmetic exception in untrusted-PAL code. This should never happen in a correct implementation
of Graphene. In this case, `_DkResumeSighandler` simply kills the faulting thread (not the whole
process!) by issuing `exit(1)` syscall.
DkGenericSignalHandle Logic
"""""""""""""""""""""""""""
::
Normal context (enclave mode)
+----------------------------------+
+ _DkGenericSignalHandle(PAL_EVENT_SUSPEND, frame/ctx)
| +
| | upcall = _DkGetExceptionHandler(PAL_EVENT_SUSPEND)
| | = suspend_upcall
| |
| | _DkGenericEventTrigger(PAL_EVENT_SUSPEND, suspend_upcall, frame/ctx)
| | +
| | | event = struct exception_event(event_num = PAL_EVENT_SUSPEND,
| | | context = ctx, +--------------+
| | | frame = frame) | only one of |
| | | | context/frame|
| | | suspend_upcall(event, ctx) | is not NULL |
| | | + +------------------------------+ +--------------+
| | | | | event is opaque ptr to LibOS |
| | | | +------------------------------+
| | | |
| | | | +------------------- PAL -> LibOS transition --------------------+
| | | |
| | | | <... LibOS signal handling ...>
| | | |
| | | | DkExceptionReturn(event)
| | | | +
| | | | | +----------------- LibOS -> PAL transition --------------------+
| | | | |
| | | | | _DkExceptionReturn(event)
| | | | | +
| | | | | | if event.frame is not NULL:
| | | | | | update regs with event.frame regs
| | | | | | return to LibOS (as if PAL function returned)
| | | | | |
| | | | | | if event.context is not NULL:
| | | | | | update regs with event.context regs (including RSP)
| | | | | | return to interrupted-context frame (somewhere in user app)
+ + + + + +
< context is reset, no unwinding here! >
Linux PAL Flows
^^^^^^^^^^^^^^^
Initialization of Signal Handling
"""""""""""""""""""""""""""""""""
Very similar to the flow for Linux-SGX. In addition to 7 handled signals, Linux PAL also operates on
these signals:
* SIGCHLD -- is ignored
* SIGPIPE -- installs `_DkPipeSighandler` handler
Describing flows for these signals is *future work*.
Async Signal Arrives During PAL Call Execution
""""""""""""""""""""""""""""""""""""""""""""""
::
Normal context Signal context
+----------------+ +----------------+
+ ... PAL code ...
|
| +-----+
| | SIGINT
| <-----+
| signal handler called
| +----------------------------------------> _DkTerminateSighandler(SIGINT, siginfo, uc)
| +
| | < SIGINT arrived during PAL call >
| |
| | add to thread's pending events:
| | tcb.pending_event = PAL_EVENT_SUSPEND
| |
| signal handler done | append PAL_EVENT_SUSPEND to tcb.pending_queue
| <----------------------------------------+ + if tcb.pending_event is already set
|
| ... PAL call finishes (LEAVE_PAL_CALL) ...
|
| __check_pending_event()
| +
| | _DkGenericSignalHandle(tcb.pending_event,
| | siginfo_t = NULL,
| | ucontext_t = NULL)
| |
| | foreach event in tcb.pending_queue:
+ + _DkGenericSignalHandle(event, NULL, NULL)
Async Signal Arrives During Non-PAL Call Execution
""""""""""""""""""""""""""""""""""""""""""""""""""
::
Normal context Signal context
+----------------+ +----------------+
+ ... non-PAL code ...
|
| +-----+
| | SIGINT
| <-----+
| signal handler called
| +----------------------------------------> _DkTerminateSighandler(SIGINT, siginfo, uc)
| +
| | < SIGINT arrived during app/LibOS code >
| |
| | _DkGenericSignalHandle(PAL_EVENT_SUSPEND,
| | + siginfo_t = NULL,
| | | ucontext_t = uc)
| | |
| | | < ... >
| signal handler done | |
| <----------------------------------------+ + +
+
Sync Signal Arrives During PAL Call Execution
"""""""""""""""""""""""""""""""""""""""""""""
::
Normal context Signal context
+----------------+ +----------------+
+ ... PAL code ...
|
| +-----+
| | SIGILL
| <-----+
| signal handler called
| +----------------------------------------> __DkGenericSighandler(SIGILL, siginfo, uc)
| +
| | < SIGILL arrived during PAL call >
| |
| | print panic message
| |
| | _DkThreadExit() < kill this thread >
... thread is dead ...
Sync Signal Arrives During Non-PAL Call Execution
"""""""""""""""""""""""""""""""""""""""""""""""""
::
Normal context Signal context
+----------------+ +----------------+
+ ... non-PAL code ...
|
| +-----+
| | SIGILL
| <-----+
| signal handler called
| +----------------------------------------> __DkGenericSighandler(SIGILL, siginfo, uc)
| +
| | < SIGILL arrived during app/LibOS code >
| |
| | _DkGenericSignalHandle(PAL_EVENT_ILLEGAL,
| | + siginfo_t = siginfo,
| | | ucontext_t = uc)
| | |
| | | < ... >
| signal handler done | |
| <----------------------------------------+ + +
+
DkGenericSignalHandle Logic
"""""""""""""""""""""""""""
::
Normal context (enclave mode)
+----------------------------------+
+ _DkGenericSignalHandle(PAL_EVENT_SUSPEND, uc)
| +
| | upcall = _DkGetExceptionHandler(PAL_EVENT_SUSPEND)
| | = suspend_upcall
| |
| | _DkGenericEventTrigger(PAL_EVENT_SUSPEND, suspend_upcall, uc)
| | +
| | | event = struct exception_event(event_num = PAL_EVENT_SUSPEND,
| | | context = copy-of-uc-regs,
| | | uc = uc)
| | |
| | | suspend_upcall(event, ctx)
| | | + +------------------------------+
| | | | | event is opaque ptr to LibOS |
| | | | +------------------------------+
| | | |
| | | | +------------------- PAL -> LibOS transition --------------------+
| | | |
| | | | <... LibOS signal handling ...>
| | | |
| | | | DkExceptionReturn(event)
| | | | +
| | | | | +----------------- LibOS -> PAL transition --------------------+
| | | | |
| | | | | _DkExceptionReturn(event)
| | | | | +
| | | | | + update event.uc.regs with event.context regs
| | | | + +-----------------------------------------------+
| | | + | unlike SGX PAL, don't jump to updated context |
| | + | but unwind call stack as usual |
| + +-----------------------------------------------+
|
| ... host OS switches Graphene to normal context if was in signal context
+ (or simply continue execution if already in normal context) ...
Current LibOS Flows
^^^^^^^^^^^^^^^^^^^
Note that LibOS flows are the same for all PALs.
Non-Nested Signal Case
""""""""""""""""""""""
On the example of `suspend_upcall()`.
::
Normal context (enclave mode, non-nested signal)
+-----------------------------------------------------+
+ suspend_upcall(event, context)
|
| if internal Graphene thread (async or ipc helper):
| DkExceptionReturn(event)
|
| siginfo_t info = (SIGINT, SI_USER, .si_pid = 0)
|
| deliver_signal(info, context = NULL)
| +
| | tcb.context.preempt = 1 < __disable_preemt() >
| |
| | shim_signal signal = (siginfo_t info = info,
| | context_stored = false/true,
| | context = LibOS-syscall/context,
| | pal_context = context = NULL)
| | +-----------------------------------------------------+
| | | If signal is delivered while in LibOS syscall, |
| | | then signal.context = LibOS-syscall context; |
| | | otherwise context = NULL and context_stored = false |
| | +-----------------------------------------------------+
| |
| | if curr-thread's signal mask includes SIGINT (blocks it):
| | < allocate_signal_log(SIGINT) and append signal to it >
| |
| | else:
| | __handle_signal(SIGINT, signal.context) < deliver pending >
| | +
| | | for each pending SIGINT signal on this thread:
| | + __handle_one_signal(SIGINT, pending-signal)
| |
| | __handle_one_signal(SIGINT, signal) < deliver this signal >
| | +
| | | save LibOS-syscall context and reset it (to indicate that
| | | context is now not LibOS but user signal handler)
| | |
| | | user signal handler(SIGINT, signal.info, signal.context)
| | | < ... >
| | |
| | | copy signal.context.<regs> in signal.pal_context if not NULL
| | + (propagate user-updated regs to event.context in DkExceptionReturn)
| |
| + tcb.context.preemt = 0 < __enable_preempt() >
|
+ DkExceptionReturn(event)
Nested Signal Case
""""""""""""""""""
On the example of `suspend_upcall()`. Assumes `tcb.context.preempt = 1` (in a signal handler).
::
Normal context (enclave mode, nested signal)
+-----------------------------------------------------+
+ suspend_upcall(event, context)
|
| if internal Graphene thread (async or ipc helper):
| DkExceptionReturn(event)
|
| siginfo_t info = (SIGINT, SI_USER, .si_pid = 0)
|
| deliver_signal(info, context = NULL)
| +
| | tcb.context.preempt = 2 < __disable_preemt() >
| |
| | shim_signal signal = (siginfo_t info = info,
| | context_stored = false/true,
| | context = LibOS+syscall/context,
| | pal_context = context = NULL)
| | +-----------------------------------------------------+
| | | If signal is delivered while in LibOS syscall, |
| | | then signal.context = LibOS+syscall context; |
| | | otherwise context = NULL and context_stored = false |
| | +-----------------------------------------------------+
| |
| | +-----------Now different from non-nested case--------------+
| |
| | < goto delay because tcb.context.preempt > 1 >
| |
| | allocate_signal_log(SIGINT):
| |
| | append signal to tcb.thread.signal_logs[SIGINT]
| |
| | tcb.thread.has_signal = 1 (increment from 0)
| |
| + tcb.context.preemt = 1 < __enable_preempt() >
|
+ DkExceptionReturn(event)
< ...after top-level signal handler is finished... >
< ...after any system call (END_SHIM) or any internal unlock... >
+ handle_signal(false)
| +
| | __handle_signal(signal-num = 0, context = NULL)
| | +
| | | for each pending (any) signal on this thread:
| | | __handle_one_signal(signo, pending-signal)
| | | +
+ + + + < handles pended SIGINT from tcb.thread.signal_logs[SIGINT]
Available Signal Handlers and Their Differences
"""""""""""""""""""""""""""""""""""""""""""""""
(Notation: <Linux signal> -> PAL signal -> LibOS signal handler (purpose))
Sync signals:
* SIGFPE -> PAL_EVENT_ARITHMETIC_ERROR -> arithmetic_error_upcall (if not
internal fault, handle pending non-blocked SIGFPEs and then this SIGFPE)
* SIGSEGV -> PAL_EVENT_MEMFAULT -> memfault_upcall (if not internal fault,
handle pending non-blocked SIGSEGVs and then this SIGSEGV)
* SIGBUS -> PAL_EVENT_MEMFAULT -> memfault_upcall (if not internal fault,
handle pending non-blocked SIGBUSs and then this SIGBUS)
* SIGILL -> PAL_EVENT_ILLEGAL -> illegal_upcall (handle pending non-blocked
SIGILLs and then this SIGILL)
Async signals:
* SIGTERM -> PAL_EVENT_QUIT -> quit_upcall (handle pending non-blocked
SIGTERMs and then this SIGTERM)
* SIGINT -> PAL_EVENT_SUSPEND -> suspend_upcall (handle pending non-blocked
SIGINTs and then this SIGINT)
* SIGCONT -> PAL_EVENT_RESUME -> resume_upcall (handle pending non-blocked
signals but not SIGCONT itself)
We already described flows of `suspend_upcall`. Here is how other signal handlers are different
from `suspend_upcall`:
::
Normal context (enclave mode)
+-----------------------------------------------------+
quit_upcall(event, context)
+
+ < exactly the same as suspend_upcall >
+-----------------------------+
resume_upcall(event, context) | handles all pending signals |
+ +-----------------------------+
| if internal Graphene thread (async or ipc helper):
| DkExceptionReturn(event)
|
| if tcb.context.preempt > 0: (nested signal)
| DkExceptionReturn(event)
|
| tcb.context.preempt = 1 < __disable_preemt() >
|
| __handle_signal(signal-code = 0, context = NULL)
| +
| | for each pending (any) signal on this thread:
| + __handle_one_signal(signo, pending-signal)
|
| tcb.context.preemt = 0 < __enable_preempt() >
|
+ DkExceptionReturn(event)
arithmetic_error_upcall(event, context)
+
| if internal Graphene thread or exception during LibOS/PAL:
| print panic message
| DkExceptionReturn(event)
|
| siginfo_t info = (SIGFPE, FPE_INTDIV,
| si_addr = <faulting addr from PAL>)
|
| deliver_signal(info, context) +--------------------------+
| < ... as in suspend_upcall ... > | note that context is set |
| +--------------------------+
+ DkExceptionReturn(event)
memfault_upcall(event, context)
+
| if exception during test_user_memory/string:
| update RIP to ret_fault
| DkExceptionReturn(event)
|
| if internal Graphene thread or exception during LibOS/PAL:
| print panic message
| DkExceptionReturn(event)
|
| < choose SIGBUS/SIGSEGV and signal code based on VMA info >
|
| siginfo_t info = (SIGBUS/SIGSEGV, signal code,
| si_addr = <faulting addr from PAL>)
|
| deliver_signal(info, context)
+ < ... as in suspend_upcall ... >
illegal_upcall(event, context)
+
| if internal Graphene thread or exception during LibOS/PAL:
| print panic message
| DkExceptionReturn(event)
|
| siginfo_t info = (SIGILL, ILL_ILLOPC,
| si_addr = <faulting addr from PAL>)
|
| deliver_signal(info, context)
| < ... as in suspend_upcall ... >
|
+ DkExceptionReturn(event)
Alarm() Emulation
-----------------
SIGALRM signal is blocked in Graphene. Therefore, on `alarm()` syscall, SIGALRM is generated and
raised purely by LibOS.
::
Application thread AsyncHelperThread
+---------------------+ +---------------------+
shim_do_alarm(seconds) ... no alive host thread ...
+ ... (created on-demand) ...
| install_async_event(seconds,
| + callback = signal_alarm)
| |
| | time = DkSystemTimeQuery()
| |
| | event = struct async_event(
| | callback = signal_alarm,
| | caller = app-thread,
| | install_time = time,
| | expire_time = time+seconds)
| |
| | append event to global async_list
| |
| | create_async_helper() < if not alive >
| | +
| | | thread_create(shim_async_helper)
| | | +
| | + + <creates new thread in host> +------> shim_async_helper()
| | +
| | set_event(async_helper_event) | while (true):
| | + | DkStreamsWaitEvents(array =
+ + + DkStreamWrite(async_helper_event) +-+ | { global async_helper_event },
| | timeout = <some-constant>)
... app-thread code continues ... | | ...
| |
+--> | event = async_list.pop()
|
| DkStreamsWaitEvents(...,
| timeout = event.expire_time)
|
| ... sleep until timeout ...
|
| timeout fired: call event.callback
|
| signal_alarm(event.caller)
| +
| | append_signal(app-thread, SIGALRM,
| | + wakeup = true)
| | |
| | | shim_signal signal = (siginfo_t info = NULL,
| | | context_stored = false,
| | | context = NULL,
| | | pal_context = NULL)
| | |
| | | < allocate_signal_log(SIGALRM) and append signal >
| | |
| | | DkThreadResume(app-thread)
| | | +
< SIGCONT delivered > <---------------------+ | + + + < send SIGCONT to app-thread via tgkill() >
|
< resume_upcall() with pending SIGALRM, | ...
see other diagrams > +
Bugs and Issues
---------------
* BUG? Graphene LibOS performs `DkThreadYieldExecution()` in `__handle_signal()` (i.e., yield
thread execution after handling one pending signal). Looks useless.
* TODO: clean-up `install_async_event()`, redundant logic in `async_list` checking
* TODO: `suspend_on_signal` is useless
* BUG? `return_from_ocall` remembers RDI = -PAL_ERROR_INTERRUPTED, but `_DkExceptionReturn` never
returns back to after `_DkHandleExternalEvent` in `return_from_ocall`. Thus, the PAL return code
(interrupted error) is lost! Check it with printfs and simple example.
* BUG? `SIGNAL_DELAYED` flag is useless? It is set as one of the highest bits in int64
`SIGNAL_DELAYED = 0x80000000UL`. `resume_upcall` sets SIGNAL_DELAYED flag in current thread's
`context.preempt` if the SIGCONT signal arrives during signal handling. `handle_signal` does the same.
* TODO: Sigsuspend fix ( https://github.com/oscarlab/graphene/issues/453 ). In `shim_do_sigsuspend`:
1. unlock before thread_setwait + thread_sleep
2. lock and unlock around last set_sig_mask
3. add code similar to `__handle_signal`, but on all possible signal numbers and without
`DkThreadYieldExecution` and without unsetting `SIGNAL_DELAYED` (?).
Allow all pending signals to be delivered
(see https://stackoverflow.com/questions/40592066/sigsuspend-vs-additional-signals-delivered-during-handler-execution).
If at least one signal was delivered, do NOT go to `thread_sleep` but immediately return
(and set the old mask beforehand).
+14 -8
View File
@@ -6,10 +6,23 @@
quickstart
manifest-syntax
debugging
glossary
howto-doc
.. toctree::
:caption: Manual pages
:maxdepth: 1
:glob:
manpages/*
.. toctree::
:caption: Developing Graphene
:maxdepth: 1
devel/debugging
devel/signal-handling
.. toctree::
:caption: LibOS
@@ -20,13 +33,6 @@
pal/host-abi
.. toctree::
:caption: Manual pages
:maxdepth: 1
:glob:
manpages/*
.. _old-wiki:
.. toctree::
@@ -1,741 +0,0 @@
# Signal Handling in Graphene
(Disclaimer: This explanation is partially outdated. It is intended only as an internal
reference for developers of Graphene, not as a general documentation for Graphene users.)
## Signal Handling
This analysis is written while Graphene's signal handling mechanisms are in flux. In future, all
Graphene PALs should implement the same mechanism, and LibOS should adopt a better scheme to
support nested signals and alternate signal stacks.
In the interest of space and mental sanity, we do not discuss FreeBSD PAL implementation.
Historically, Linux and FreeBSD shared the same mechanism (where signals were immediately delivered
to LibOS even if signal arrived during PAL call). This old mechanism was adopted by Linux-SGX PAL,
though due to peculiarities of Intel SGX, it has its own sub-flows and is more complicated.
Currently, Linux PAL implements a new mechanism where a signal during a PAL call is pended and is
delivered to LibOS only after the PAL call is finished.
So, there are two signal-handling mechanisms at the PAL layer:
* Linux PAL: (1) If signal arrives during PAL call, pend it and return from signal context,
continuing normal context of PAL call. Immediately after a PAL call is finished, deliver all
pending signals to LibOS. (2) If signal arrives during LibOS/application code, deliver the
signal immediately to LibOS. Note that the signal delivery and handling is done in signal context
(in contrast to pending-signal delivery).
* Linux-SGX PAL: (1) If signal arrives during enclave-code execution, remember the interrupted
enclave-code context and return from signal context. When jumping back into the enclave (in normal
context), deliver the signal to LibOS. After handling the signal, LibOS/PAL will continue from
interrupted enclave-code context. (2) If signal arrives during non-enclave-code, i.e.
untrusted-PAL, execution, just return from signal context. When jumping back into the enclave
(in normal context), deliver the signal to LibOS. In contrast to first case, after handling the
signal, LibOS/PAL will continue as if outermost PAL function failed with `PAL_ERROR_INTERRUPTED`.
The advantage of the first mechanism is that there is never a possibility of nested PAL calls
(which is not supported by Graphene). However, this also disallows nested signals already at the
PAL layer. The advantage of the second mechanism is that nested signals are possible, at least as
far as it concerns the PAL layer.
There is a single unified signal-handling mechanism at the LibOS layer. This mechanism does *not*
support nested signals: if a signal is delivered while another signal is handled (or during a LibOS
internal lock), then it is pended. Pended signals are delivered after any system-call completion
or after any LibOS internal unlock.
A new signal-handling mechanism at the LibOS layer was proposed by Isaku Yamahata
(see https://github.com/oscarlab/graphene/pull/347). This proposal changes the points at which
signals are delivered to the user app. The two points are (1) if signal arrives during app
execution, the signal is delivered after host OS returns from signal context, and (2) if signal
arrives during LibOS/PAL execution, the signal is delivered after system-call completion. This is
in contrast to current LibOS approach of (1) delivering the first signal even in the middle of
emulated syscall, and (2) pending nested signals until system-call completion.
### Linux-SGX PAL Flows
#### Initialization of Signal Handling
```
Normal context
+----------------+
... load_enclave() ...
+
| sgx_signal_setup()
| +
| | set_sighandler(SIGTERM | SIGINT | SIGCONT) +---------------+
| | + | async signals |
| | | action = struct sigaction( +---------------+
| | | sa_handler = _DkTerminateSighandler,
| | | sa_restorer = rt_sigreturn,
| | | sa_flags = {SA_SIGINFO | SA_RESTORER}
| | | sa_mask = {SIGCONT}
| | | )
| | |
| | | rt_sigaction(SIGTERM, action)
| | | rt_sigaction(SIGINT, action)
| | | rt_sigaction(SIGCONT, action)
| | |
| | + rt_sigprocmask(SIGUNBLOCK, SIGTERM | SIGINT | SIGCONT)
| |
| | set_sighandler(SIGSEGV | SIGILL | SIGFPE | SIGBUS) +--------------+
| | + | sync signals |
| | | action = struct sigaction( +--------------+
| | | sa_handler = _DkResumeSighandler,
| | | sa_restorer = rt_sigreturn,
| | | sa_flags = {SA_SIGINFO | SA_RESTORER}
| | | sa_mask = {SIGCONT}
| | | )
| | |
| | | rt_sigaction(SIGSEGV, action)
| | | rt_sigaction(SIGILL, action)
| | | rt_sigaction(SIGFPE, action)
| | | rt_sigaction(SIGBUS, action)
| | |
+ + + rt_sigprocmask(SIGUNBLOCK, SIGSEGV | SIGILL | SIGFPE | SIGBUS)
```
#### Async Signal Arrives During Enclave Code Execution
On the example of SIGINT, until we arrive into `_DkGenericSignalHandle()`.
```
Normal context Signal context
+----------------+ +----------------+
+ ... enclave code .....
|
| +-----+
| | AEX due to SIGINT
| <-----+
| signal handler called
| +----------------------------------------> _DkTerminateSighandler(SIGINT, siginfo, uc)
| +
| | sgx_raise(PAL_EVENT_SUSPEND)
| | +
| | | RDX = after_resume() addr
| | | RBX = current thread's TCS
| | | RCX = async_exit_pointer() addr
| | | RDI = PAL_EVENT_SUSPEND (from first func arg)
| | |
| | | EENTER(RBX, RCX) <---+
| | | + |
| | | | (SGX creates new SSA frame and |
| | | | sets RAX = current-SSA-frame = 1) |e
| | | | |n
| | | | enclave_entry() |c
| | | | + |l
| | | | | jump to handle_resume() |a
| | | | | +>
| | | | | <double-check RDI contains signum > |e
| | | | | |
| | | | | jump to handle_exception() |m
| | | | | |o
| | | | | create new enclave-thread stack frame |d
| | | | | and push GPRSGX registers on this frame |e
| | | | | |
| | | | | update GPRSGX = (RSP = new frame, |
| | RDI = PAL_EVENT_SUSPEND, |
| | | | | RSI = new frame, |
| | | | | RIP = _DkExceptionHandler) |
| | | | | |
| | | + + EEXIT(RDX = after_resume) <---+
| signal handler done | |
| <----------------------------------------+ + + after_resume(): return
|
| async_exit_pointer()
| +
| | ERESUME <---+
| | + |
| | | (SGX's current-SSA-frame = 0, thus |e
| | | enclave-thread's GPRSGX is loaded in regs) |n
| | | |c
| | | _DkExceptionHandler(exit_info = RDI = PAL_EVENT_SUSPEND, uc = RSI = new frame) |l
| | | + |a
| | | | PAL_CONTEXT ctx = copy(uc) <ctx contains interrupted-context frame> |v
| | | | |e
+ + + + _DkGenericSignalHandle(PAL_EVENT_SUSPEND, ctx) v
< ... >
```
#### Async Signal Arrives During Non-Enclave Code Execution
Non-enclave code execution can only happen if Graphene process is currently executing untrusted-PAL
code, e.g., is blocked on a `futex(wait)` system call.
On the example of SIGINT, until we arrive into `_DkGenericSignalHandle()`.
```
Normal context Signal context
+----------------+ +----------------+
+ ... non-enclave code ...
|
| +-----+
| | SIGINT
| <-----+
| signal handler called
| +----------------------------------------> _DkTerminateSighandler(SIGINT, siginfo, uc)
| +
| | update normal-context registers =
| | (RIP = sgx_entry_return,
| | RDI = -PAL_ERROR_INTERRUPTED,
| signal handler done | RSI = PAL_EVENT_SUSPEND)
| <----------------------------------------+ +
|
| sgx_entry_return()
| +
| | RDX = sgx_entry() addr
| | RBX = current thread's TCS
| | RCX = async_exit_pointer() addr
| |
| | EENTER(RBX, RCX) <--+
| | + |
| | | (SGX's current-SSA-frame = 0, ocall is done) |
| | | |e
| | | enclave_entry() |n
| | | + |c
| | | | jump to return_from_ocall() |l
| | | | |a
| | | | remember RDI = -PAL_ERROR_INTERRUPTED on enclave stack |v
| | | | |e
| | | | _DkHandleExternalEvent(event = RDI = PAL_EVENT_SUSPEND, |
| | | | + uc = RSI = enclave frame) |m
| | | | | |o
| | | | | frame = get_frame(uc) <finds outermost PAL function> |d
| | | | | |e
| | | | | _DkGenericSignalHandle(PAL_EVENT_SUSPEND, frame) |
< ... > v
```
#### Sync Signal Arrives During Enclave Code Execution
This case is exactly the same as for async signal. The only difference in the diagram would be that
`_DkTerminateSighandler` is replaced by `_DkResumeSighandler`. But the logic is exactly the same.
#### Sync Signal Arrives During Non-Enclave Code Execution
Non-enclave code execution can only happen if Graphene process is currently executing untrusted-PAL
code, e.g., is blocked on a `futex(wait)` system call.
If a sync signal arrives in this case, it means that there was a memory fault, illegal instruction,
or arithmetic exception in untrusted-PAL code. This should never happen in a correct implementation
of Graphene. In this case, `_DkResumeSighandler` simply kills the faulting thread (not the whole
process!) by issuing `exit(1)` syscall.
#### DkGenericSignalHandle Logic
```
Normal context (enclave mode)
+----------------------------------+
+ _DkGenericSignalHandle(PAL_EVENT_SUSPEND, frame/ctx)
| +
| | upcall = _DkGetExceptionHandler(PAL_EVENT_SUSPEND)
| | = suspend_upcall
| |
| | _DkGenericEventTrigger(PAL_EVENT_SUSPEND, suspend_upcall, frame/ctx)
| | +
| | | event = struct exception_event(event_num = PAL_EVENT_SUSPEND,
| | | context = ctx, +--------------+
| | | frame = frame) | only one of |
| | | | context/frame|
| | | suspend_upcall(event, ctx) | is not NULL |
| | | + +------------------------------+ +--------------+
| | | | | event is opaque ptr to LibOS |
| | | | +------------------------------+
| | | |
| | | | +------------------- PAL -> LibOS transition --------------------+
| | | |
| | | | <... LibOS signal handling ...>
| | | |
| | | | DkExceptionReturn(event)
| | | | +
| | | | | +----------------- LibOS -> PAL transition --------------------+
| | | | |
| | | | | _DkExceptionReturn(event)
| | | | | +
| | | | | | if event.frame is not NULL:
| | | | | | update regs with event.frame regs
| | | | | | return to LibOS (as if PAL function returned)
| | | | | |
| | | | | | if event.context is not NULL:
| | | | | | update regs with event.context regs (including RSP)
| | | | | | return to interrupted-context frame (somewhere in user app)
+ + + + + +
< context is reset, no unwinding here! >
```
### Linux PAL Flows
#### Initialization of Signal Handling
Very similar to the flow for Linux-SGX. In addition to 7 handled signals, Linux PAL also operates on
these signals:
* SIGCHLD -- is ignored
* SIGPIPE -- installs `_DkPipeSighandler` handler
Describing flows for these signals is *future work*.
#### Async Signal Arrives During PAL Call Execution
```
Normal context Signal context
+----------------+ +----------------+
+ ... PAL code ...
|
| +-----+
| | SIGINT
| <-----+
| signal handler called
| +----------------------------------------> _DkTerminateSighandler(SIGINT, siginfo, uc)
| +
| | < SIGINT arrived during PAL call >
| |
| | add to thread's pending events:
| | tcb.pending_event = PAL_EVENT_SUSPEND
| |
| signal handler done | append PAL_EVENT_SUSPEND to tcb.pending_queue
| <----------------------------------------+ + if tcb.pending_event is already set
|
| ... PAL call finishes (LEAVE_PAL_CALL) ...
|
| __check_pending_event()
| +
| | _DkGenericSignalHandle(tcb.pending_event,
| | siginfo_t = NULL,
| | ucontext_t = NULL)
| |
| | foreach event in tcb.pending_queue:
+ + _DkGenericSignalHandle(event, NULL, NULL)
```
#### Async Signal Arrives During Non-PAL Call Execution
```
Normal context Signal context
+----------------+ +----------------+
+ ... non-PAL code ...
|
| +-----+
| | SIGINT
| <-----+
| signal handler called
| +----------------------------------------> _DkTerminateSighandler(SIGINT, siginfo, uc)
| +
| | < SIGINT arrived during app/LibOS code >
| |
| | _DkGenericSignalHandle(PAL_EVENT_SUSPEND,
| | + siginfo_t = NULL,
| | | ucontext_t = uc)
| | |
| | | < ... >
| signal handler done | |
| <----------------------------------------+ + +
+
```
#### Sync Signal Arrives During PAL Call Execution
```
Normal context Signal context
+----------------+ +----------------+
+ ... PAL code ...
|
| +-----+
| | SIGILL
| <-----+
| signal handler called
| +----------------------------------------> __DkGenericSighandler(SIGILL, siginfo, uc)
| +
| | < SIGILL arrived during PAL call >
| |
| | print panic message
| |
| | _DkThreadExit() < kill this thread >
... thread is dead ...
```
#### Sync Signal Arrives During Non-PAL Call Execution
```
Normal context Signal context
+----------------+ +----------------+
+ ... non-PAL code ...
|
| +-----+
| | SIGILL
| <-----+
| signal handler called
| +----------------------------------------> __DkGenericSighandler(SIGILL, siginfo, uc)
| +
| | < SIGILL arrived during app/LibOS code >
| |
| | _DkGenericSignalHandle(PAL_EVENT_ILLEGAL,
| | + siginfo_t = siginfo,
| | | ucontext_t = uc)
| | |
| | | < ... >
| signal handler done | |
| <----------------------------------------+ + +
+
```
#### DkGenericSignalHandle Logic
```
Normal context (enclave mode)
+----------------------------------+
+ _DkGenericSignalHandle(PAL_EVENT_SUSPEND, uc)
| +
| | upcall = _DkGetExceptionHandler(PAL_EVENT_SUSPEND)
| | = suspend_upcall
| |
| | _DkGenericEventTrigger(PAL_EVENT_SUSPEND, suspend_upcall, uc)
| | +
| | | event = struct exception_event(event_num = PAL_EVENT_SUSPEND,
| | | context = copy-of-uc-regs,
| | | uc = uc)
| | |
| | | suspend_upcall(event, ctx)
| | | + +------------------------------+
| | | | | event is opaque ptr to LibOS |
| | | | +------------------------------+
| | | |
| | | | +------------------- PAL -> LibOS transition --------------------+
| | | |
| | | | <... LibOS signal handling ...>
| | | |
| | | | DkExceptionReturn(event)
| | | | +
| | | | | +----------------- LibOS -> PAL transition --------------------+
| | | | |
| | | | | _DkExceptionReturn(event)
| | | | | +
| | | | | + update event.uc.regs with event.context regs
| | | | + +-----------------------------------------------+
| | | + | unlike SGX PAL, don't jump to updated context |
| | + | but unwind call stack as usual |
| + +-----------------------------------------------+
|
| ... host OS switches Graphene to normal context if was in signal context
+ (or simply continue execution if already in normal context) ...
```
### Current LibOS Flows
Note that LibOS flows are the same for all PALs.
#### Non-Nested Signal Case
On the example of `suspend_upcall()`.
```
Normal context (enclave mode, non-nested signal)
+-----------------------------------------------------+
+ suspend_upcall(event, context)
|
| if internal Graphene thread (async or ipc helper):
| DkExceptionReturn(event)
|
| siginfo_t info = (SIGINT, SI_USER, .si_pid = 0)
|
| deliver_signal(info, context = NULL)
| +
| | tcb.context.preempt = 1 < __disable_preemt() >
| |
| | shim_signal signal = (siginfo_t info = info,
| | context_stored = false/true,
| | context = LibOS-syscall/context,
| | pal_context = context = NULL)
| | +-----------------------------------------------------+
| | | If signal is delivered while in LibOS syscall, |
| | | then signal.context = LibOS-syscall context; |
| | | otherwise context = NULL and context_stored = false |
| | +-----------------------------------------------------+
| |
| | if curr-thread's signal mask includes SIGINT (blocks it):
| | < allocate_signal_log(SIGINT) and append signal to it >
| |
| | else:
| | __handle_signal(SIGINT, signal.context) < deliver pending >
| | +
| | | for each pending SIGINT signal on this thread:
| | + __handle_one_signal(SIGINT, pending-signal)
| |
| | __handle_one_signal(SIGINT, signal) < deliver this signal >
| | +
| | | save LibOS-syscall context and reset it (to indicate that
| | | context is now not LibOS but user signal handler)
| | |
| | | user signal handler(SIGINT, signal.info, signal.context)
| | | < ... >
| | |
| | | copy signal.context.<regs> in signal.pal_context if not NULL
| | + (propagate user-updated regs to event.context in DkExceptionReturn)
| |
| + tcb.context.preemt = 0 < __enable_preempt() >
|
+ DkExceptionReturn(event)
```
#### Nested Signal Case
On the example of `suspend_upcall()`. Assumes `tcb.context.preempt = 1` (in a signal handler).
```
Normal context (enclave mode, nested signal)
+-----------------------------------------------------+
+ suspend_upcall(event, context)
|
| if internal Graphene thread (async or ipc helper):
| DkExceptionReturn(event)
|
| siginfo_t info = (SIGINT, SI_USER, .si_pid = 0)
|
| deliver_signal(info, context = NULL)
| +
| | tcb.context.preempt = 2 < __disable_preemt() >
| |
| | shim_signal signal = (siginfo_t info = info,
| | context_stored = false/true,
| | context = LibOS+syscall/context,
| | pal_context = context = NULL)
| | +-----------------------------------------------------+
| | | If signal is delivered while in LibOS syscall, |
| | | then signal.context = LibOS+syscall context; |
| | | otherwise context = NULL and context_stored = false |
| | +-----------------------------------------------------+
| |
| | +-----------Now different from non-nested case--------------+
| |
| | < goto delay because tcb.context.preempt > 1 >
| |
| | allocate_signal_log(SIGINT):
| |
| | append signal to tcb.thread.signal_logs[SIGINT]
| |
| | tcb.thread.has_signal = 1 (increment from 0)
| |
| + tcb.context.preemt = 1 < __enable_preempt() >
|
+ DkExceptionReturn(event)
< ...after top-level signal handler is finished... >
< ...after any system call (END_SHIM) or any internal unlock... >
+ handle_signal(false)
| +
| | __handle_signal(signal-num = 0, context = NULL)
| | +
| | | for each pending (any) signal on this thread:
| | | __handle_one_signal(signo, pending-signal)
| | | +
+ + + + < handles pended SIGINT from tcb.thread.signal_logs[SIGINT]
```
#### Available Signal Handlers and Their Differences
(Notation: <Linux signal> -> PAL signal -> LibOS signal handler (purpose))
Sync signals:
* SIGFPE -> PAL_EVENT_ARITHMETIC_ERROR -> arithmetic_error_upcall (if not internal fault, handle
pending non-blocked SIGFPEs and then this SIGFPE)
* SIGSEGV -> PAL_EVENT_MEMFAULT -> memfault_upcall (if not internal fault, handle pending
non-blocked SIGSEGVs and then this SIGSEGV)
* SIGBUS -> PAL_EVENT_MEMFAULT -> memfault_upcall (if not internal fault, handle pending
non-blocked SIGBUSs and then this SIGBUS)
* SIGILL -> PAL_EVENT_ILLEGAL -> illegal_upcall (handle pending non-blocked SIGILLs and then
this SIGILL)
Async signals:
* SIGTERM -> PAL_EVENT_QUIT -> quit_upcall (handle pending non-blocked SIGTERMs and then
this SIGTERM)
* SIGINT -> PAL_EVENT_SUSPEND -> suspend_upcall (handle pending non-blocked SIGINTs and then
this SIGINT)
* SIGCONT -> PAL_EVENT_RESUME -> resume_upcall (handle pending non-blocked signals but not
SIGCONT itself)
We already described flows of `suspend_upcall`. Here is how other signal handlers are different
from `suspend_upcall`:
```
Normal context (enclave mode)
+-----------------------------------------------------+
quit_upcall(event, context)
+
+ < exactly the same as suspend_upcall >
+-----------------------------+
resume_upcall(event, context) | handles all pending signals |
+ +-----------------------------+
| if internal Graphene thread (async or ipc helper):
| DkExceptionReturn(event)
|
| if tcb.context.preempt > 0: (nested signal)
| DkExceptionReturn(event)
|
| tcb.context.preempt = 1 < __disable_preemt() >
|
| __handle_signal(signal-code = 0, context = NULL)
| +
| | for each pending (any) signal on this thread:
| + __handle_one_signal(signo, pending-signal)
|
| tcb.context.preemt = 0 < __enable_preempt() >
|
+ DkExceptionReturn(event)
arithmetic_error_upcall(event, context)
+
| if internal Graphene thread or exception during LibOS/PAL:
| print panic message
| DkExceptionReturn(event)
|
| siginfo_t info = (SIGFPE, FPE_INTDIV,
| si_addr = <faulting addr from PAL>)
|
| deliver_signal(info, context) +--------------------------+
| < ... as in suspend_upcall ... > | note that context is set |
| +--------------------------+
+ DkExceptionReturn(event)
memfault_upcall(event, context)
+
| if exception during test_user_memory/string:
| update RIP to ret_fault
| DkExceptionReturn(event)
|
| if internal Graphene thread or exception during LibOS/PAL:
| print panic message
| DkExceptionReturn(event)
|
| < choose SIGBUS/SIGSEGV and signal code based on VMA info >
|
| siginfo_t info = (SIGBUS/SIGSEGV, signal code,
| si_addr = <faulting addr from PAL>)
|
| deliver_signal(info, context)
+ < ... as in suspend_upcall ... >
illegal_upcall(event, context)
+
| if internal Graphene thread or exception during LibOS/PAL:
| print panic message
| DkExceptionReturn(event)
|
| siginfo_t info = (SIGILL, ILL_ILLOPC,
| si_addr = <faulting addr from PAL>)
|
| deliver_signal(info, context)
| < ... as in suspend_upcall ... >
|
+ DkExceptionReturn(event)
```
## Alarm() Emulation
SIGALRM signal is blocked in Graphene. Therefore, on `alarm()` syscall, SIGALRM is generated and
raised purely by LibOS.
```
Application thread AsyncHelperThread
+---------------------+ +---------------------+
shim_do_alarm(seconds) ... no alive host thread ...
+ ... (created on-demand) ...
| install_async_event(seconds,
| + callback = signal_alarm)
| |
| | time = DkSystemTimeQuery()
| |
| | event = struct async_event(
| | callback = signal_alarm,
| | caller = app-thread,
| | install_time = time,
| | expire_time = time+seconds)
| |
| | append event to global async_list
| |
| | create_async_helper() < if not alive >
| | +
| | | thread_create(shim_async_helper)
| | | +
| | + + <creates new thread in host> +------> shim_async_helper()
| | +
| | set_event(async_helper_event) | while (true):
| | + | DkStreamsWaitEvents(array =
+ + + DkStreamWrite(async_helper_event) +-+ | { global async_helper_event },
| | timeout = <some-constant>)
... app-thread code continues ... | | ...
| |
+--> | event = async_list.pop()
|
| DkStreamsWaitEvents(...,
| timeout = event.expire_time)
|
| ... sleep until timeout ...
|
| timeout fired: call event.callback
|
| signal_alarm(event.caller)
| +
| | append_signal(app-thread, SIGALRM,
| | + wakeup = true)
| | |
| | | shim_signal signal = (siginfo_t info = NULL,
| | | context_stored = false,
| | | context = NULL,
| | | pal_context = NULL)
| | |
| | | < allocate_signal_log(SIGALRM) and append signal >
| | |
| | | DkThreadResume(app-thread)
| | | +
< SIGCONT delivered > <---------------------+ | + + + < send SIGCONT to app-thread via tgkill() >
|
< resume_upcall() with pending SIGALRM, | ...
see other diagrams > +
```
## Bugs and Issues
* BUG? Graphene LibOS performs `DkThreadYieldExecution()` in `__handle_signal()` (i.e., yield
thread execution after handling one pending signal). Looks useless.
* TODO: clean-up `install_async_event()`, redundant logic in `async_list` checking
* TODO: `suspend_on_signal` is useless
* BUG? `return_from_ocall` remembers RDI = -PAL_ERROR_INTERRUPTED, but `_DkExceptionReturn` never
returns back to after `_DkHandleExternalEvent` in `return_from_ocall`. Thus, the PAL return code
(interrupted error) is lost! Check it with printfs and simple example.
* BUG? `SIGNAL_DELAYED` flag is useless? It is set as one of the highest bits in int64
`SIGNAL_DELAYED = 0x80000000UL`. `resume_upcall` sets SIGNAL_DELAYED flag in current thread's
`context.preempt` if the SIGCONT signal arrives during signal handling. `handle_signal` does the same.
* TODO: Sigsuspend fix ( https://github.com/oscarlab/graphene/issues/453 ). In `shim_do_sigsuspend`:
(1) unlock before thread_setwait + thread_sleep
(2) lock and unlock around last set_sig_mask
(3) add code similar to `__handle_signal`, but on all possible signal numbers and without
`DkThreadYieldExecution` and without unsetting `SIGNAL_DELAYED` (?).
Allow all pending signals to be delivered
(see https://stackoverflow.com/questions/40592066/sigsuspend-vs-additional-signals-delivered-during-handler-execution).
If at least one signal was delivered, do NOT go to `thread_sleep` but immediately return
(and set the old mask beforehand).