From: "Alex Bennée" <alex.bennee@linaro.org>
To: Josh Kunz <jkz@google.com>
Cc: riku.voipio@iki.fi, qemu-devel@nongnu.org, laurent@vivier.eu
Subject: Re: [PATCH 1/5] linux-user: Refactor do_fork to use new `qemu_clone`
Date: Tue, 16 Jun 2020 16:51:45 +0100 [thread overview]
Message-ID: <87mu53ypny.fsf@linaro.org> (raw)
In-Reply-To: <20200612014606.147691-2-jkz@google.com>
Josh Kunz <jkz@google.com> writes:
> This is pre-work for adding full support for the `CLONE_VM` `clone`
> flag. In a follow-up patch, we'll add support to `clone.c` for
> `clone_vm`-type clones beyond threads. CLONE_VM support is more
> complicated, so first we're splitting existing clone mechanisms
> (pthread_create, and fork) into a separate file.
>
> Signed-off-by: Josh Kunz <jkz@google.com>
> ---
> linux-user/Makefile.objs | 2 +-
> linux-user/clone.c | 152 ++++++++++++++++
> linux-user/clone.h | 27 +++
> linux-user/syscall.c | 376 +++++++++++++++++++--------------------
> 4 files changed, 365 insertions(+), 192 deletions(-)
> create mode 100644 linux-user/clone.c
> create mode 100644 linux-user/clone.h
>
> diff --git a/linux-user/Makefile.objs b/linux-user/Makefile.objs
> index 1940910a73..d6788f012c 100644
> --- a/linux-user/Makefile.objs
> +++ b/linux-user/Makefile.objs
> @@ -1,7 +1,7 @@
> obj-y = main.o syscall.o strace.o mmap.o signal.o \
> elfload.o linuxload.o uaccess.o uname.o \
> safe-syscall.o $(TARGET_ABI_DIR)/signal.o \
> - $(TARGET_ABI_DIR)/cpu_loop.o exit.o fd-trans.o
> + $(TARGET_ABI_DIR)/cpu_loop.o exit.o fd-trans.o clone.o
>
> obj-$(TARGET_HAS_BFLT) += flatload.o
> obj-$(TARGET_I386) += vm86.o
> diff --git a/linux-user/clone.c b/linux-user/clone.c
> new file mode 100644
> index 0000000000..f02ae8c464
> --- /dev/null
> +++ b/linux-user/clone.c
> @@ -0,0 +1,152 @@
> +#include "qemu/osdep.h"
> +#include "qemu.h"
> +#include "clone.h"
> +#include <pthread.h>
> +#include <sched.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <sys/syscall.h>
> +#include <sys/types.h>
> +#include <sys/wait.h>
> +#include <unistd.h>
> +#include <stdbool.h>
> +#include <assert.h>
> +
> +static const unsigned long NEW_STACK_SIZE = 0x40000UL;
> +
> +/*
> + * A completion tracks an event that can be completed. It's based on the
> + * kernel concept with the same name, but implemented with userspace locks.
> + */
> +struct completion {
> + /* done is set once this completion has been completed. */
> + bool done;
> + /* mu syncronizes access to this completion. */
> + pthread_mutex_t mu;
> + /* cond is used to broadcast completion status to awaiting threads. */
> + pthread_cond_t cond;
> +};
> +
> +static void completion_init(struct completion *c)
> +{
> + c->done = false;
> + pthread_mutex_init(&c->mu, NULL);
> + pthread_cond_init(&c->cond, NULL);
> +}
> +
> +/*
> + * Block until the given completion finishes. Returns immediately if the
> + * completion has already finished.
> + */
> +static void completion_await(struct completion *c)
> +{
> + pthread_mutex_lock(&c->mu);
> + if (c->done) {
> + pthread_mutex_unlock(&c->mu);
> + return;
> + }
> + pthread_cond_wait(&c->cond, &c->mu);
> + assert(c->done && "returned from cond wait without being marked as done");
> + pthread_mutex_unlock(&c->mu);
> +}
This introduces another sync mechanism specifically for clone behaviour
- is there a reason one of the exiting mechanisms can't be used? If this
brings new useful functionality it might be worth introducing it as a
system wide function.
> +
> +/*
> + * Finish the completion. Unblocks all awaiters.
> + */
> +static void completion_finish(struct completion *c)
> +{
> + pthread_mutex_lock(&c->mu);
> + assert(!c->done && "trying to finish an already finished completion");
> + c->done = true;
> + pthread_cond_broadcast(&c->cond);
> + pthread_mutex_unlock(&c->mu);
> +}
> +
> +struct clone_thread_info {
> + struct completion running;
> + int tid;
> + int (*callback)(void *);
> + void *payload;
> +};
> +
> +static void *clone_thread_run(void *raw_info)
> +{
> + struct clone_thread_info *info = (struct clone_thread_info *) raw_info;
> + info->tid = syscall(SYS_gettid);
> +
> + /*
> + * Save out callback/payload since lifetime of info is only guaranteed
> + * until we finish the completion.
> + */
> + int (*callback)(void *) = info->callback;
> + void *payload = info->payload;
> + completion_finish(&info->running);
> +
> + _exit(callback(payload));
> +}
> +
> +static int clone_thread(int flags, int (*callback)(void *), void
> *payload)
It's nicer to typedef a function call type rather than manually casting
to it each time.
> +{
> + struct clone_thread_info info;
> + pthread_attr_t attr;
> + int ret;
> + pthread_t thread_unused;
> +
> + memset(&info, 0, sizeof(info));
> +
> + completion_init(&info.running);
> + info.callback = callback;
> + info.payload = payload;
> +
> + (void)pthread_attr_init(&attr);
> + (void)pthread_attr_setstacksize(&attr, NEW_STACK_SIZE);
> + (void)pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
> +
> + ret = pthread_create(&thread_unused, &attr, clone_thread_run, (void *) &info);
> + /* pthread_create returns errors directly, instead of via errno. */
> + if (ret != 0) {
> + errno = ret;
> + ret = -1;
> + } else {
> + completion_await(&info.running);
> + ret = info.tid;
> + }
> +
> + pthread_attr_destroy(&attr);
> + return ret;
> +}
> +
> +int qemu_clone(int flags, int (*callback)(void *), void *payload)
> +{
> + int ret;
> +
> + if (clone_flags_are_thread(flags)) {
> + /*
> + * The new process uses the same flags as pthread_create, so we can
> + * use pthread_create directly. This is an optimization.
> + */
> + return clone_thread(flags, callback, payload);
> + }
> +
> + if (clone_flags_are_fork(flags)) {
> + /*
> + * Special case a true `fork` clone call. This is so we can take
> + * advantage of special pthread_atfork handlers in libraries we
> + * depend on (e.g., glibc). Without this, existing users of `fork`
> + * in multi-threaded environments will likely get new flaky
> + * deadlocks.
> + */
> + fork_start();
> + ret = fork();
> + if (ret == 0) {
> + fork_end(1);
> + _exit(callback(payload));
> + }
> + fork_end(0);
> + return ret;
> + }
> +
> + /* !fork && !thread */
> + errno = EINVAL;
> + return -1;
> +}
> diff --git a/linux-user/clone.h b/linux-user/clone.h
> new file mode 100644
> index 0000000000..34ae9b3780
> --- /dev/null
> +++ b/linux-user/clone.h
> @@ -0,0 +1,27 @@
> +#ifndef CLONE_H
> +#define CLONE_H
> +
> +/*
> + * qemu_clone executes the given `callback`, with the given payload as the
> + * first argument, in a new process created with the given flags. Some clone
> + * flags, such as *SETTLS, *CLEARTID are not supported. The child thread ID is
> + * returned on success, otherwise negative errno is returned on clone failure.
> + */
> +int qemu_clone(int flags, int (*callback)(void *), void *payload);
> +
> +/* Returns true if the given clone flags can be emulated with libc fork. */
> +static bool clone_flags_are_fork(unsigned int flags)
> +{
> + return flags == SIGCHLD;
> +}
> +
> +/* Returns true if the given clone flags can be emulated with pthread_create. */
> +static bool clone_flags_are_thread(unsigned int flags)
> +{
> + return flags == (
> + CLONE_VM | CLONE_FS | CLONE_FILES |
> + CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM
> + );
> +}
This is fine in of itself but we seemed to have lost some context from
moving the flags from syscall.c.
> +
> +#endif /* CLONE_H */
> diff --git a/linux-user/syscall.c b/linux-user/syscall.c
> index 97de9fb5c9..7ce021cea2 100644
> --- a/linux-user/syscall.c
> +++ b/linux-user/syscall.c
> @@ -122,6 +122,7 @@
> #include "qapi/error.h"
> #include "fd-trans.h"
> #include "tcg/tcg.h"
> +#include "clone.h"
>
> #ifndef CLONE_IO
> #define CLONE_IO 0x80000000 /* Clone io context */
> @@ -135,12 +136,6 @@
> * * flags we can implement within QEMU itself
> * * flags we can't support and will return an error for
> */
> -/* For thread creation, all these flags must be present; for
> - * fork, none must be present.
> - */
> -#define CLONE_THREAD_FLAGS \
> - (CLONE_VM | CLONE_FS | CLONE_FILES | \
> - CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM)
>
> /* These flags are ignored:
> * CLONE_DETACHED is now ignored by the kernel;
> @@ -150,30 +145,10 @@
> (CLONE_DETACHED | CLONE_IO)
>
> /* Flags for fork which we can implement within QEMU itself */
> -#define CLONE_OPTIONAL_FORK_FLAGS \
> +#define CLONE_EMULATED_FLAGS \
> (CLONE_SETTLS | CLONE_PARENT_SETTID | \
> CLONE_CHILD_CLEARTID | CLONE_CHILD_SETTID)
>
> -/* Flags for thread creation which we can implement within QEMU itself */
> -#define CLONE_OPTIONAL_THREAD_FLAGS \
> - (CLONE_SETTLS | CLONE_PARENT_SETTID | \
> - CLONE_CHILD_CLEARTID | CLONE_CHILD_SETTID | CLONE_PARENT)
> -
> -#define CLONE_INVALID_FORK_FLAGS \
> - (~(CSIGNAL | CLONE_OPTIONAL_FORK_FLAGS | CLONE_IGNORED_FLAGS))
> -
> -#define CLONE_INVALID_THREAD_FLAGS \
> - (~(CSIGNAL | CLONE_THREAD_FLAGS | CLONE_OPTIONAL_THREAD_FLAGS | \
> - CLONE_IGNORED_FLAGS))
> -
> -/* CLONE_VFORK is special cased early in do_fork(). The other flag bits
> - * have almost all been allocated. We cannot support any of
> - * CLONE_NEWNS, CLONE_NEWCGROUP, CLONE_NEWUTS, CLONE_NEWIPC,
> - * CLONE_NEWUSER, CLONE_NEWPID, CLONE_NEWNET, CLONE_PTRACE, CLONE_UNTRACED.
> - * The checks against the invalid thread masks above will catch these.
> - * (The one remaining unallocated bit is 0x1000 which used to be CLONE_PID.)
> - */
> -
I think some of the above can be usefully moved to clone.h to discuss
the various clone/fork options QEMU can and can't support.
> /* Define DEBUG_ERESTARTSYS to force every syscall to be restarted
> * once. This exercises the codepaths for restart.
> */
> @@ -1104,7 +1079,7 @@ static inline rlim_t target_to_host_rlim(abi_ulong target_rlim)
> {
> abi_ulong target_rlim_swap;
> rlim_t result;
> -
> +
> target_rlim_swap = tswapal(target_rlim);
> if (target_rlim_swap == TARGET_RLIM_INFINITY)
> return RLIM_INFINITY;
> @@ -1112,7 +1087,7 @@ static inline rlim_t target_to_host_rlim(abi_ulong target_rlim)
> result = target_rlim_swap;
> if (target_rlim_swap != (rlim_t)result)
> return RLIM_INFINITY;
> -
> +
> return result;
> }
> #endif
> @@ -1122,13 +1097,13 @@ static inline abi_ulong host_to_target_rlim(rlim_t rlim)
> {
> abi_ulong target_rlim_swap;
> abi_ulong result;
> -
> +
> if (rlim == RLIM_INFINITY || rlim != (abi_long)rlim)
> target_rlim_swap = TARGET_RLIM_INFINITY;
> else
> target_rlim_swap = rlim;
> result = tswapal(target_rlim_swap);
> -
> +
> return result;
> }
> #endif
> @@ -1615,10 +1590,11 @@ static inline abi_long target_to_host_cmsg(struct msghdr *msgh,
> abi_ulong target_cmsg_addr;
> struct target_cmsghdr *target_cmsg, *target_cmsg_start;
> socklen_t space = 0;
> -
> +
> msg_controllen = tswapal(target_msgh->msg_controllen);
> - if (msg_controllen < sizeof (struct target_cmsghdr))
> + if (msg_controllen < sizeof(struct target_cmsghdr)) {
> goto the_end;
> + }
> target_cmsg_addr = tswapal(target_msgh->msg_control);
> target_cmsg = lock_user(VERIFY_READ, target_cmsg_addr, msg_controllen, 1);
> target_cmsg_start = target_cmsg;
> @@ -1703,8 +1679,9 @@ static inline abi_long host_to_target_cmsg(struct target_msghdr *target_msgh,
> socklen_t space = 0;
>
> msg_controllen = tswapal(target_msgh->msg_controllen);
> - if (msg_controllen < sizeof (struct target_cmsghdr))
> + if (msg_controllen < sizeof(struct target_cmsghdr)) {
> goto the_end;
> + }
Try and avoid un-related whitespace fixes in code that is otherwise unchanged.
> target_cmsg_addr = tswapal(target_msgh->msg_control);
> target_cmsg = lock_user(VERIFY_WRITE, target_cmsg_addr, msg_controllen, 0);
> target_cmsg_start = target_cmsg;
> @@ -5750,9 +5727,10 @@ abi_long do_set_thread_area(CPUX86State *env, abi_ulong ptr)
> }
> unlock_user_struct(target_ldt_info, ptr, 1);
>
> - if (ldt_info.entry_number < TARGET_GDT_ENTRY_TLS_MIN ||
> - ldt_info.entry_number > TARGET_GDT_ENTRY_TLS_MAX)
> - return -TARGET_EINVAL;
> + if (ldt_info.entry_number < TARGET_GDT_ENTRY_TLS_MIN ||
> + ldt_info.entry_number > TARGET_GDT_ENTRY_TLS_MAX) {
> + return -TARGET_EINVAL;
> + }
> seg_32bit = ldt_info.flags & 1;
> contents = (ldt_info.flags >> 1) & 3;
> read_exec_only = (ldt_info.flags >> 3) & 1;
> @@ -5828,7 +5806,7 @@ static abi_long do_get_thread_area(CPUX86State *env, abi_ulong ptr)
> lp = (uint32_t *)(gdt_table + idx);
> entry_1 = tswap32(lp[0]);
> entry_2 = tswap32(lp[1]);
> -
> +
> read_exec_only = ((entry_2 >> 9) & 1) ^ 1;
> contents = (entry_2 >> 10) & 3;
> seg_not_present = ((entry_2 >> 15) & 1) ^ 1;
> @@ -5844,8 +5822,8 @@ static abi_long do_get_thread_area(CPUX86State *env, abi_ulong ptr)
> (read_exec_only << 3) | (limit_in_pages << 4) |
> (seg_not_present << 5) | (useable << 6) | (lm << 7);
> limit = (entry_1 & 0xffff) | (entry_2 & 0xf0000);
> - base_addr = (entry_1 >> 16) |
> - (entry_2 & 0xff000000) |
> + base_addr = (entry_1 >> 16) |
> + (entry_2 & 0xff000000) |
> ((entry_2 & 0xff) << 16);
> target_ldt_info->base_addr = tswapal(base_addr);
> target_ldt_info->limit = tswap32(limit);
> @@ -5895,53 +5873,71 @@ abi_long do_arch_prctl(CPUX86State *env, int code, abi_ulong addr)
>
> #endif /* defined(TARGET_I386) */
>
> -#define NEW_STACK_SIZE 0x40000
> -
> -
> static pthread_mutex_t clone_lock = PTHREAD_MUTEX_INITIALIZER;
> typedef struct {
> - CPUArchState *env;
> + /* Used to synchronize thread/process creation between parent and child. */
> pthread_mutex_t mutex;
> pthread_cond_t cond;
> - pthread_t thread;
> - uint32_t tid;
> + /*
> + * Guest pointers for implementing CLONE_PARENT_SETTID
> + * and CLONE_CHILD_SETTID.
> + */
> abi_ulong child_tidptr;
> abi_ulong parent_tidptr;
> - sigset_t sigmask;
> -} new_thread_info;
> + struct {
> + sigset_t sigmask;
> + CPUArchState *env;
> + bool register_thread;
> + bool signal_setup;
> + } child;
> +} clone_info;
>
> -static void *clone_func(void *arg)
> +static int clone_run(void *arg)
> {
> - new_thread_info *info = arg;
> + clone_info *info = (clone_info *) arg;
> CPUArchState *env;
> CPUState *cpu;
> TaskState *ts;
> + uint32_t tid;
>
> - rcu_register_thread();
> - tcg_register_thread();
> - env = info->env;
> + if (info->child.register_thread) {
> + rcu_register_thread();
> + tcg_register_thread();
> + }
> +
> + env = info->child.env;
> cpu = env_cpu(env);
> thread_cpu = cpu;
> ts = (TaskState *)cpu->opaque;
> - info->tid = sys_gettid();
> + tid = sys_gettid();
> task_settid(ts);
> - if (info->child_tidptr)
> - put_user_u32(info->tid, info->child_tidptr);
> - if (info->parent_tidptr)
> - put_user_u32(info->tid, info->parent_tidptr);
> +
> qemu_guest_random_seed_thread_part2(cpu->random_seed);
> - /* Enable signals. */
> - sigprocmask(SIG_SETMASK, &info->sigmask, NULL);
> - /* Signal to the parent that we're ready. */
> - pthread_mutex_lock(&info->mutex);
> - pthread_cond_broadcast(&info->cond);
> - pthread_mutex_unlock(&info->mutex);
> - /* Wait until the parent has finished initializing the tls state. */
> - pthread_mutex_lock(&clone_lock);
> - pthread_mutex_unlock(&clone_lock);
> +
> + if (info->parent_tidptr) {
> + /*
> + * Even when memory is not shared, parent_tidptr is set before the
> + * process copy, so we need to set it in the child.
> + */
> + put_user_u32(tid, info->parent_tidptr);
> + }
> +
> + if (info->child_tidptr) {
> + put_user_u32(tid, info->child_tidptr);
> + }
> +
> + /* Enable signals. */
> + sigprocmask(SIG_SETMASK, &info->child.sigmask, NULL);
> +
> + if (info->child.signal_setup) {
> + pthread_mutex_lock(&info->mutex);
> + pthread_cond_broadcast(&info->cond);
> + pthread_mutex_unlock(&info->mutex);
> + }
> +
> cpu_loop(env);
> /* never exits */
> - return NULL;
> + _exit(1); /* avoid compiler warning. */
> }
>
> /* do_fork() Must return host values and target errnos (unlike most
> @@ -5951,139 +5947,131 @@ static int do_fork(CPUArchState *env, unsigned int flags, abi_ulong newsp,
> abi_ulong child_tidptr)
> {
> CPUState *cpu = env_cpu(env);
> - int ret;
> + int proc_flags, host_sig, ret;
> TaskState *ts;
> CPUState *new_cpu;
> - CPUArchState *new_env;
> - sigset_t sigmask;
> + sigset_t block_sigmask;
> + sigset_t orig_sigmask;
> + clone_info info;
> + TaskState *parent_ts = (TaskState *)cpu->opaque;
>
> - flags &= ~CLONE_IGNORED_FLAGS;
> + memset(&info, 0, sizeof(info));
> +
> + /*
> + * When cloning the actual subprocess, we don't need to worry about any
> + * flags that can be ignored, or emulated in QEMU. proc_flags holds only
> + * the flags that need to be passed to `clone` itself.
> + */
> + proc_flags = flags & ~(CLONE_EMULATED_FLAGS | CLONE_IGNORED_FLAGS);
> +
> + /*
> + * The exit signal is included in the flags. That signal needs to be mapped
> + * to the appropriate host signal, and we need to check if that signal is
> + * supported.
> + */
> + host_sig = target_to_host_signal(proc_flags & CSIGNAL);
> + if (host_sig > SIGRTMAX) {
> + qemu_log_mask(LOG_UNIMP,
> + "guest signal %d not supported for exit_signal",
> + proc_flags & CSIGNAL);
> + return -TARGET_EINVAL;
> + }
> + proc_flags = (proc_flags & ~CSIGNAL) | host_sig;
>
> /* Emulate vfork() with fork() */
> - if (flags & CLONE_VFORK)
> - flags &= ~(CLONE_VFORK | CLONE_VM);
> + if (proc_flags & CLONE_VFORK) {
> + proc_flags &= ~(CLONE_VFORK | CLONE_VM);
> + }
>
> - if (flags & CLONE_VM) {
> - TaskState *parent_ts = (TaskState *)cpu->opaque;
> - new_thread_info info;
> - pthread_attr_t attr;
> + if (!clone_flags_are_fork(proc_flags) &&
> + !clone_flags_are_thread(proc_flags)) {
> + qemu_log_mask(LOG_UNIMP, "unsupported clone flags");
> + return -TARGET_EINVAL;
> + }
>
> - if (((flags & CLONE_THREAD_FLAGS) != CLONE_THREAD_FLAGS) ||
> - (flags & CLONE_INVALID_THREAD_FLAGS)) {
> - return -TARGET_EINVAL;
> - }
> + pthread_mutex_init(&info.mutex, NULL);
> + pthread_mutex_lock(&info.mutex);
> + pthread_cond_init(&info.cond, NULL);
>
> - ts = g_new0(TaskState, 1);
> - init_task_state(ts);
> + ts = g_new0(TaskState, 1);
> + init_task_state(ts);
>
> - /* Grab a mutex so that thread setup appears atomic. */
> - pthread_mutex_lock(&clone_lock);
> + /* Guard CPU copy. It is not thread-safe. */
Why not - isn't that the point of the lock?
<snip>
> case TARGET_NR_setdomainname:
> if (!(p = lock_user_string(arg1)))
> @@ -10873,8 +10865,10 @@ static abi_long do_syscall1(void *cpu_env, int num, abi_long arg1,
> return get_errno(fchown(arg1, low2highuid(arg2), low2highgid(arg3)));
> #if defined(TARGET_NR_fchownat)
> case TARGET_NR_fchownat:
> - if (!(p = lock_user_string(arg2)))
> + p = lock_user_string(arg2)
> + if (!p) {
> return -TARGET_EFAULT;
> + }
This has dropped a ; killing the build
--
Alex Bennée
next prev parent reply other threads:[~2020-06-16 15:52 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2020-06-12 1:46 [PATCH 0/5] linux-user: Support extended clone(CLONE_VM) Josh Kunz
2020-06-12 1:46 ` [PATCH 1/5] linux-user: Refactor do_fork to use new `qemu_clone` Josh Kunz
2020-06-16 15:51 ` Alex Bennée [this message]
2020-06-12 1:46 ` [PATCH 2/5] linux-user: Make fd_trans task-specific Josh Kunz
2020-06-12 1:46 ` [PATCH 3/5] linux-user: Make sigact_table part of the task state Josh Kunz
2020-06-12 1:46 ` [PATCH 4/5] linux-user: Support CLONE_VM and extended clone options Josh Kunz
2020-06-13 0:10 ` Josh Kunz
2020-06-16 16:08 ` Alex Bennée
2020-06-23 3:43 ` Josh Kunz
2020-06-23 8:21 ` Alex Bennée
[not found] ` <CADgy-2tB0Z133RB1i8OdnpKMD3xATL059dFoduHOjdim11G4-A@mail.gmail.com>
[not found] ` <87k0zw7opa.fsf@linaro.org>
2020-07-09 0:16 ` Josh Kunz
2020-07-16 10:41 ` Alex Bennée
2020-06-12 1:46 ` [PATCH 5/5] linux-user: Add PDEATHSIG test for clone process hierarchy Josh Kunz
2020-06-12 3:48 ` [PATCH 0/5] linux-user: Support extended clone(CLONE_VM) no-reply
2020-06-13 11:16 ` Alex Bennée
2020-06-16 16:02 ` Alex Bennée
2020-06-16 16:32 ` Peter Maydell
2020-06-16 23:38 ` Josh Kunz
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=87mu53ypny.fsf@linaro.org \
--to=alex.bennee@linaro.org \
--cc=jkz@google.com \
--cc=laurent@vivier.eu \
--cc=qemu-devel@nongnu.org \
--cc=riku.voipio@iki.fi \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).