aboutsummaryrefslogtreecommitdiffstats
path: root/executor/common.h
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2018-07-20 20:26:05 +0200
committerDmitry Vyukov <dvyukov@google.com>2018-07-24 12:04:27 +0200
commit9fe4bdc5f1037a409e82299f36117030114c7b94 (patch)
treed3d73c1f69ded8152436be47684a07baa0e7f6ec /executor/common.h
parentdb7957bc09bf5715d33e4c56b8614579aa94000a (diff)
executor: overhaul
Make as much code as possible shared between all OSes. In particular main is now common across all OSes. Make more code shared between executor and csource (in particular, loop function and threaded execution logic). Also make loop and threaded logic shared across all OSes. Make more posix/unix code shared across OSes (e.g. signal handling, pthread creation, etc). Plus other changes along similar lines. Also support test OS in executor (based on portable posix) and add 4 arches that cover all execution modes (fork server/no fork server, shmem/no shmem). This change paves way for testing of executor code and allows to preserve consistency across OSes and executor/csource.
Diffstat (limited to 'executor/common.h')
-rw-r--r--executor/common.h587
1 files changed, 477 insertions, 110 deletions
diff --git a/executor/common.h b/executor/common.h
index 59812411e..9f7552900 100644
--- a/executor/common.h
+++ b/executor/common.h
@@ -3,151 +3,293 @@
// This file is shared between executor and csource package.
-#include <stdint.h>
-#include <string.h>
-#if defined(SYZ_EXECUTOR) || (defined(SYZ_REPEAT) && defined(SYZ_WAIT_REPEAT)) || \
- defined(SYZ_USE_TMP_DIR) || defined(SYZ_TUN_ENABLE) || defined(SYZ_SANDBOX_NAMESPACE) || \
- defined(SYZ_SANDBOX_NONE) || defined(SYZ_SANDBOX_SETUID) || defined(SYZ_FAULT_INJECTION) || \
- defined(__NR_syz_kvm_setup_cpu) || defined(__NR_syz_init_net_socket) || \
- defined(__NR_syz_mmap)
-#include <errno.h>
-#include <stdarg.h>
-#include <stdio.h>
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
#endif
-#if defined(SYZ_EXECUTOR) || defined(SYZ_USE_TMP_DIR)
+
+#include <endian.h> // for htobe*.
+#include <stdint.h>
+#include <stdio.h> // for fmt arguments
#include <stdlib.h>
-#include <sys/stat.h>
+#include <string.h>
+
+#if SYZ_EXECUTOR && !GOOS_linux
+#include <unistd.h>
+NORETURN void doexit(int status)
+{
+ _exit(status);
+ for (;;) {
+ }
+}
#endif
-#if defined(SYZ_EXECUTOR) || defined(SYZ_HANDLE_SEGV)
+
+#if !GOOS_fuchsia && !GOOS_windows
+#if SYZ_EXECUTOR || SYZ_HANDLE_SEGV
#include <setjmp.h>
#include <signal.h>
#include <string.h>
+
+#if GOOS_linux
+#include <sys/syscall.h>
#endif
-#if defined(SYZ_EXECUTOR) || defined(SYZ_DEBUG)
-#include <stdarg.h>
-#include <stdio.h>
+
+static __thread int skip_segv;
+static __thread jmp_buf segv_env;
+
+#if GOOS_akaros
+#include <parlib/parlib.h>
+static void recover()
+{
+ _longjmp(segv_env, 1);
+}
#endif
-#if defined(SYZ_EXECUTOR)
-// exit/_exit do not necessary work (e.g. if fuzzer sets seccomp filter that prohibits exit_group).
-// Use doexit instead. We must redefine exit to something that exists in stdlib,
-// because some standard libraries contain "using ::exit;", but has different signature.
-#define exit vsnprintf
-#define _exit vsnprintf
-
-// uint64 is impossible to printf without using the clumsy and verbose "%" PRId64.
-// So we define and use uint64. Note: pkg/csource does s/uint64/uint64/.
-// Also define uint32/16/8 for consistency.
-typedef unsigned long long uint64;
-typedef unsigned int uint32;
-typedef unsigned short uint16;
-typedef unsigned char uint8;
-
-#ifdef SYZ_EXECUTOR
-// Note: zircon max fd is 256.
-const int kInPipeFd = 250; // remapped from stdin
-const int kOutPipeFd = 251; // remapped from stdout
-#endif
-
-#if defined(__GNUC__)
-#define SYSCALLAPI
-#define NORETURN __attribute__((noreturn))
-#define ALIGNED(N) __attribute__((aligned(N)))
-#define PRINTF __attribute__((format(printf, 1, 2)))
+static void segv_handler(int sig, siginfo_t* info, void* ctx)
+{
+ // Generated programs can contain bad (unmapped/protected) addresses,
+ // which cause SIGSEGVs during copyin/copyout.
+ // This handler ignores such crashes to allow the program to proceed.
+ // We additionally opportunistically check that the faulty address
+ // is not within executable data region, because such accesses can corrupt
+ // output region and then fuzzer will fail on corrupted data.
+ uintptr_t addr = (uintptr_t)info->si_addr;
+ const uintptr_t prog_start = 1 << 20;
+ const uintptr_t prog_end = 100 << 20;
+ if (__atomic_load_n(&skip_segv, __ATOMIC_RELAXED) && (addr < prog_start || addr > prog_end)) {
+ debug("SIGSEGV on %p, skipping\n", (void*)addr);
+#if GOOS_akaros
+ struct user_context* uctx = (struct user_context*)ctx;
+ uctx->tf.hw_tf.tf_rip = (long)(void*)recover;
+ return;
#else
-// Assuming windows/cl.
-#define SYSCALLAPI WINAPI
-#define NORETURN __declspec(noreturn)
-#define ALIGNED(N) __declspec(align(N))
-#define PRINTF
+ _longjmp(segv_env, 1);
#endif
+ }
+ debug("SIGSEGV on %p, exiting\n", (void*)addr);
+ doexit(sig);
+}
-typedef long(SYSCALLAPI* syscall_t)(long, long, long, long, long, long, long, long, long);
+static void install_segv_handler()
+{
+ struct sigaction sa;
+#if GOOS_linux
+ // Don't need that SIGCANCEL/SIGSETXID glibc stuff.
+ // SIGCANCEL sent to main thread causes it to exit
+ // without bringing down the whole group.
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = SIG_IGN;
+ syscall(SYS_rt_sigaction, 0x20, &sa, NULL, 8);
+ syscall(SYS_rt_sigaction, 0x21, &sa, NULL, 8);
+#endif
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_sigaction = segv_handler;
+ sa.sa_flags = SA_NODEFER | SA_SIGINFO;
+ sigaction(SIGSEGV, &sa, NULL);
+ sigaction(SIGBUS, &sa, NULL);
+}
-struct call_t {
- const char* name;
- int sys_nr;
- syscall_t call;
-};
+#define NONFAILING(...) \
+ { \
+ __atomic_fetch_add(&skip_segv, 1, __ATOMIC_SEQ_CST); \
+ if (_setjmp(segv_env) == 0) { \
+ __VA_ARGS__; \
+ } \
+ __atomic_fetch_sub(&skip_segv, 1, __ATOMIC_SEQ_CST); \
+ }
+#endif
+#endif
-#endif // #if defined(SYZ_EXECUTOR)
+#if !GOOS_windows
+#if SYZ_EXECUTOR || SYZ_THREADED || SYZ_REPEAT
+static void sleep_ms(uint64 ms)
+{
+ usleep(ms * 1000);
+}
+#endif
-#if defined(SYZ_EXECUTOR) || (defined(SYZ_REPEAT) && defined(SYZ_WAIT_REPEAT)) || \
- defined(SYZ_USE_TMP_DIR) || defined(SYZ_TUN_ENABLE) || defined(SYZ_SANDBOX_NAMESPACE) || \
- defined(SYZ_SANDBOX_NONE) || defined(SYZ_SANDBOX_SETUID) || defined(SYZ_FAULT_INJECTION) || \
- defined(__NR_syz_kvm_setup_cpu) || defined(__NR_syz_mmap)
-const int kFailStatus = 67;
-const int kRetryStatus = 69;
+#if SYZ_EXECUTOR || SYZ_THREADED || SYZ_REPEAT
+#include <time.h>
+
+uint64 current_time_ms()
+{
+ struct timespec ts;
+ if (clock_gettime(CLOCK_MONOTONIC, &ts))
+ fail("clock_gettime failed");
+ return (uint64)ts.tv_sec * 1000 + (uint64)ts.tv_nsec / 1000000;
+}
#endif
-#if defined(SYZ_EXECUTOR)
-const int kErrorStatus = 68;
+#if SYZ_EXECUTOR || SYZ_USE_TMP_DIR
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+static void use_temporary_dir()
+{
+ char tmpdir_template[] = "./syzkaller.XXXXXX";
+ char* tmpdir = mkdtemp(tmpdir_template);
+ if (!tmpdir)
+ fail("failed to mkdtemp");
+ if (chmod(tmpdir, 0777))
+ fail("failed to chmod");
+ if (chdir(tmpdir))
+ fail("failed to chdir");
+}
#endif
+#endif
+
+#if GOOS_akaros || GOOS_netbsd || GOOS_freebsd || GOOS_test
+#if SYZ_EXECUTOR || SYZ_EXECUTOR_USES_FORK_SERVER && SYZ_REPEAT && SYZ_USE_TMP_DIR
+#include <dirent.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
-#if defined(SYZ_EXECUTOR) || (defined(SYZ_REPEAT) && defined(SYZ_WAIT_REPEAT)) || \
- defined(SYZ_USE_TMP_DIR) || defined(SYZ_TUN_ENABLE) || defined(SYZ_SANDBOX_NAMESPACE) || \
- defined(SYZ_SANDBOX_NONE) || defined(SYZ_SANDBOX_SETUID) || defined(__NR_syz_kvm_setup_cpu) || \
- defined(__NR_syz_init_net_socket) && \
- (defined(SYZ_SANDBOX_NONE) || defined(SYZ_SANDBOX_SETUID) || defined(SYZ_SANDBOX_NAMESPACE)) || \
- defined(__NR_syz_mmap)
-// logical error (e.g. invalid input program), use as an assert() alternative
-NORETURN PRINTF static void fail(const char* msg, ...)
+static void remove_dir(const char* dir)
{
- int e = errno;
- va_list args;
- va_start(args, msg);
- vfprintf(stderr, msg, args);
- va_end(args);
- fprintf(stderr, " (errno %d)\n", e);
- // ENOMEM/EAGAIN is frequent cause of failures in fuzzing context,
- // so handle it here as non-fatal error.
- doexit((e == ENOMEM || e == EAGAIN) ? kRetryStatus : kFailStatus);
+ DIR* dp;
+ struct dirent* ep;
+ dp = opendir(dir);
+ if (dp == NULL)
+ exitf("opendir(%s) failed", dir);
+ while ((ep = readdir(dp))) {
+ if (strcmp(ep->d_name, ".") == 0 || strcmp(ep->d_name, "..") == 0)
+ continue;
+ char filename[FILENAME_MAX];
+ snprintf(filename, sizeof(filename), "%s/%s", dir, ep->d_name);
+ struct stat st;
+ if (lstat(filename, &st))
+ exitf("lstat(%s) failed", filename);
+ if (S_ISDIR(st.st_mode)) {
+ remove_dir(filename);
+ continue;
+ }
+ if (unlink(filename))
+ exitf("unlink(%s) failed", filename);
+ }
+ closedir(dp);
+ if (rmdir(dir))
+ exitf("rmdir(%s) failed", dir);
}
#endif
+#endif
-#if defined(SYZ_EXECUTOR)
-// kernel error (e.g. wrong syscall return value)
-NORETURN PRINTF static void error(const char* msg, ...)
+#if !GOOS_linux
+#if SYZ_EXECUTOR || SYZ_FAULT_INJECTION
+static int inject_fault(int nth)
{
- va_list args;
- va_start(args, msg);
- vfprintf(stderr, msg, args);
- va_end(args);
- fprintf(stderr, "\n");
- doexit(kErrorStatus);
+ return 0;
}
#endif
+#if SYZ_EXECUTOR
+static int fault_injected(int fail_fd)
+{
+ return 0;
+}
+#endif
+#endif
+
+#if !GOOS_windows
+#if SYZ_EXECUTOR || SYZ_THREADED
+#include <pthread.h>
-#if defined(SYZ_EXECUTOR) || (defined(SYZ_REPEAT) && defined(SYZ_WAIT_REPEAT) && defined(SYZ_USE_TMP_DIR)) || defined(SYZ_FAULT_INJECTION)
-// just exit (e.g. due to temporal ENOMEM error)
-NORETURN PRINTF static void exitf(const char* msg, ...)
+static void thread_start(void* (*fn)(void*), void* arg)
{
- int e = errno;
- va_list args;
- va_start(args, msg);
- vfprintf(stderr, msg, args);
- va_end(args);
- fprintf(stderr, " (errno %d)\n", e);
- doexit(kRetryStatus);
+ pthread_t th;
+ pthread_attr_t attr;
+ pthread_attr_init(&attr);
+ pthread_attr_setstacksize(&attr, 128 << 10);
+ if (pthread_create(&th, &attr, fn, arg))
+ exitf("pthread_create failed");
+ pthread_attr_destroy(&attr);
}
+
+#endif
#endif
-#if defined(SYZ_EXECUTOR) || defined(SYZ_DEBUG)
-static int flag_debug;
+#if GOOS_freebsd || GOOS_netbsd || GOOS_akaros || GOOS_test
+#if SYZ_EXECUTOR || SYZ_THREADED
+
+#include <pthread.h>
+#include <time.h>
+
+typedef struct {
+ pthread_mutex_t mu;
+ pthread_cond_t cv;
+ int state;
+} event_t;
-PRINTF static void debug(const char* msg, ...)
+static void event_init(event_t* ev)
{
- if (!flag_debug)
- return;
- va_list args;
- va_start(args, msg);
- vfprintf(stderr, msg, args);
- va_end(args);
- fflush(stderr);
+ if (pthread_mutex_init(&ev->mu, 0))
+ fail("pthread_mutex_init failed");
+ if (pthread_cond_init(&ev->cv, 0))
+ fail("pthread_cond_init failed");
+ ev->state = 0;
+}
+
+static void event_reset(event_t* ev)
+{
+ ev->state = 0;
+}
+
+static void event_set(event_t* ev)
+{
+ pthread_mutex_lock(&ev->mu);
+ if (ev->state)
+ fail("event already set");
+ ev->state = 1;
+ pthread_mutex_unlock(&ev->mu);
+ pthread_cond_broadcast(&ev->cv);
+}
+
+static void event_wait(event_t* ev)
+{
+ pthread_mutex_lock(&ev->mu);
+ while (!ev->state)
+ pthread_cond_wait(&ev->cv, &ev->mu);
+ pthread_mutex_unlock(&ev->mu);
+}
+
+static int event_isset(event_t* ev)
+{
+ pthread_mutex_lock(&ev->mu);
+ int res = ev->state;
+ pthread_mutex_unlock(&ev->mu);
+ return res;
+}
+
+static int event_timedwait(event_t* ev, uint64 timeout_ms)
+{
+ struct timespec ts;
+ if (clock_gettime(CLOCK_MONOTONIC, &ts))
+ fail("clock_gettime failed");
+ const uint64 kNsPerSec = 1000 * 1000 * 1000;
+ uint64 start_ns = (uint64)ts.tv_sec * kNsPerSec + (uint64)ts.tv_nsec;
+ uint64 timeout_ns = timeout_ms * 1000 * 1000;
+ pthread_mutex_lock(&ev->mu);
+ for (;;) {
+ if (ev->state)
+ break;
+ if (clock_gettime(CLOCK_MONOTONIC, &ts))
+ fail("clock_gettime failed");
+ uint64 now_ns = (uint64)ts.tv_sec * kNsPerSec + (uint64)ts.tv_nsec;
+ if (now_ns - start_ns > timeout_ns)
+ break;
+ uint64 remain_ns = timeout_ns - (now_ns - start_ns);
+ ts.tv_sec = remain_ns / kNsPerSec;
+ ts.tv_nsec = remain_ns % kNsPerSec;
+ pthread_cond_timedwait(&ev->cv, &ev->mu, &ts);
+ }
+ int res = ev->state;
+ pthread_mutex_unlock(&ev->mu);
+ return res;
}
#endif
+#endif
-#if defined(SYZ_EXECUTOR) || defined(SYZ_USE_BITMASKS)
+#if SYZ_EXECUTOR || SYZ_USE_BITMASKS
#define BITMASK_LEN(type, bf_len) (type)((1ull << (bf_len)) - 1)
#define BITMASK_LEN_OFF(type, bf_off, bf_len) (type)(BITMASK_LEN(type, (bf_len)) << (bf_off))
@@ -163,7 +305,7 @@ PRINTF static void debug(const char* msg, ...)
}
#endif
-#if defined(SYZ_EXECUTOR) || defined(SYZ_USE_CHECKSUMS)
+#if SYZ_EXECUTOR || SYZ_USE_CHECKSUMS
struct csum_inet {
uint32 acc;
};
@@ -194,3 +336,228 @@ static uint16 csum_inet_digest(struct csum_inet* csum)
return ~csum->acc;
}
#endif
+
+#if GOOS_akaros
+#include "common_akaros.h"
+#elif GOOS_freebsd || GOOS_netbsd
+#include "common_bsd.h"
+#elif GOOS_fuchsia
+#include "common_fuchsia.h"
+#elif GOOS_linux
+#include "common_linux.h"
+#elif GOOS_test
+#include "common_test.h"
+#elif GOOS_windows
+#include "common_windows.h"
+#elif GOOS_test
+#include "common_test.h"
+#else
+#error "unknown OS"
+#endif
+
+#if SYZ_THREADED
+struct thread_t {
+ int created, call;
+ event_t ready, done;
+};
+
+static struct thread_t threads[16];
+static void execute_call(int call);
+static int running;
+#if SYZ_COLLIDE
+static int collide;
+#endif
+
+static void* thr(void* arg)
+{
+ struct thread_t* th = (struct thread_t*)arg;
+ for (;;) {
+ event_wait(&th->ready);
+ event_reset(&th->ready);
+ execute_call(th->call);
+ __atomic_fetch_sub(&running, 1, __ATOMIC_RELAXED);
+ event_set(&th->done);
+ }
+ return 0;
+}
+
+static void execute(int num_calls)
+{
+ int call, thread;
+ running = 0;
+ for (call = 0; call < num_calls; call++) {
+ for (thread = 0; thread < sizeof(threads) / sizeof(threads[0]); thread++) {
+ struct thread_t* th = &threads[thread];
+ if (!th->created) {
+ th->created = 1;
+ event_init(&th->ready);
+ event_init(&th->done);
+ event_set(&th->done);
+ thread_start(thr, th);
+ }
+ if (!event_isset(&th->done))
+ continue;
+ event_reset(&th->done);
+ th->call = call;
+ __atomic_fetch_add(&running, 1, __ATOMIC_RELAXED);
+ event_set(&th->ready);
+#if SYZ_COLLIDE
+ if (collide && (call % 2) == 0)
+ break;
+#endif
+ event_timedwait(&th->done, 25);
+ if (__atomic_load_n(&running, __ATOMIC_RELAXED))
+ sleep_ms((call == num_calls - 1) ? 10 : 2);
+ break;
+ }
+ }
+}
+#endif
+
+#if SYZ_EXECUTOR || SYZ_REPEAT
+static void execute_one();
+#if SYZ_EXECUTOR_USES_FORK_SERVER
+#include <signal.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#if GOOS_linux
+#define WAIT_FLAGS __WALL
+#else
+#define WAIT_FLAGS 0
+#endif
+
+#if SYZ_EXECUTOR
+static void reply_handshake();
+#endif
+
+static void loop()
+{
+ setup_loop();
+#if SYZ_EXECUTOR
+ // Tell parent that we are ready to serve.
+ reply_handshake();
+#endif
+#if SYZ_EXECUTOR && GOOS_akaros
+ // For akaros we do exec in the child process because new threads can't be created in the fork child.
+ // Thus we proxy input program over the child_pipe to the child process.
+ int child_pipe[2];
+ if (pipe(child_pipe))
+ fail("pipe failed");
+#endif
+ int iter;
+ for (iter = 0;; iter++) {
+#if SYZ_EXECUTOR || SYZ_USE_TMP_DIR
+ // Create a new private work dir for this test (removed at the end of the loop).
+ char cwdbuf[32];
+ sprintf(cwdbuf, "./%d", iter);
+ if (mkdir(cwdbuf, 0777))
+ fail("failed to mkdir");
+#endif
+ reset_loop();
+#if SYZ_EXECUTOR
+ receive_execute();
+#endif
+ int pid = fork();
+ if (pid < 0)
+ fail("clone failed");
+ if (pid == 0) {
+ setup_test();
+#if SYZ_EXECUTOR || SYZ_USE_TMP_DIR
+ if (chdir(cwdbuf))
+ fail("failed to chdir");
+#endif
+#if GOOS_akaros
+#if SYZ_EXECUTOR
+ dup2(child_pipe[0], kInPipeFd);
+ close(child_pipe[0]);
+ close(child_pipe[1]);
+#endif
+ execl(program_name, program_name, "child", NULL);
+ fail("execl failed");
+#else
+#if SYZ_EXECUTOR
+ close(kInPipeFd);
+#endif
+#if SYZ_EXECUTOR && SYZ_EXECUTOR_USES_SHMEM
+ close(kOutPipeFd);
+#endif
+ execute_one();
+ debug("worker exiting\n");
+ reset_test();
+ doexit(0);
+#endif
+ }
+ debug("spawned worker pid %d\n", pid);
+
+#if SYZ_EXECUTOR && GOOS_akaros
+ resend_execute(child_pipe[1]);
+#endif
+ // We used to use sigtimedwait(SIGCHLD) to wait for the subprocess.
+ // But SIGCHLD is also delivered when a process stops/continues,
+ // so it would require a loop with status analysis and timeout recalculation.
+ // SIGCHLD should also unblock the usleep below, so the spin loop
+ // should be as efficient as sigtimedwait.
+ int status = 0;
+ uint64 start = current_time_ms();
+#if SYZ_EXECUTOR && SYZ_EXECUTOR_USES_SHMEM
+ uint64 last_executed = start;
+ uint32 executed_calls = __atomic_load_n(output_data, __ATOMIC_RELAXED);
+#endif
+ for (;;) {
+ if (waitpid(-1, &status, WNOHANG | WAIT_FLAGS) == pid)
+ break;
+ sleep_ms(1);
+#if SYZ_EXECUTOR && SYZ_EXECUTOR_USES_SHMEM
+ // Even though the test process executes exit at the end
+ // and execution time of each syscall is bounded by 20ms,
+ // this backup watchdog is necessary and its performance is important.
+ // The problem is that exit in the test processes can fail (sic).
+ // One observed scenario is that the test processes prohibits
+ // exit_group syscall using seccomp. Another observed scenario
+ // is that the test processes setups a userfaultfd for itself,
+ // then the main thread hangs when it wants to page in a page.
+ // Below we check if the test process still executes syscalls
+ // and kill it after 1s of inactivity.
+ uint64 now = current_time_ms();
+ uint32 now_executed = __atomic_load_n(output_data, __ATOMIC_RELAXED);
+ if (executed_calls != now_executed) {
+ executed_calls = now_executed;
+ last_executed = now;
+ }
+ if ((now - start < 5 * 1000) && (now - start < 3 * 1000 || now - last_executed < 1000))
+ continue;
+#else
+ if (current_time_ms() - start < 5 * 1000)
+ continue;
+#endif
+ debug("killing\n");
+#if GOOS_linux
+ kill(-pid, SIGKILL);
+#endif
+ kill(pid, SIGKILL);
+ while (waitpid(-1, &status, WAIT_FLAGS) != pid) {
+ }
+ break;
+ }
+#if SYZ_EXECUTOR
+ status = WEXITSTATUS(status);
+ if (status == kFailStatus)
+ fail("child failed");
+ if (status == kErrorStatus)
+ error("child errored");
+ reply_execute(0);
+#endif
+#if SYZ_EXECUTOR || SYZ_USE_TMP_DIR
+ remove_dir(cwdbuf);
+#endif
+ }
+}
+#else
+static void loop()
+{
+ (void)sleep_ms;
+ execute_one();
+}
+#endif
+#endif