diff mbox series

[v2] plugins/stoptrigger: TCG plugin to stop execution under conditions

Message ID 20240710120854.34333-2-simon.hamelin@grenoble-inp.org (mailing list archive)
State New, archived
Headers show
Series [v2] plugins/stoptrigger: TCG plugin to stop execution under conditions | expand

Commit Message

Simon Hamelin July 10, 2024, 12:08 p.m. UTC
This new plugin allows to stop emulation using conditions on the
emulation state. By setting this plugin arguments, it is possible
to set an instruction count limit and/or trigger address(es) to stop at.
The code returned at emulation exit can be customized.

This plugin demonstrates how someone could stop QEMU execution.
It could be used for research purposes to launch some code and
deterministically stop it and understand where its execution flow went.

Co-authored-by: Alexandre Iooss <erdnaxe@crans.org>
Signed-off-by: Simon Hamelin <simon.hamelin@grenoble-inp.org>
Signed-off-by: Alexandre Iooss <erdnaxe@crans.org>
---
v2:
  - use a scoreboard for counting instructions
  - no longer hook each instruction to exit at given address
  - add `exit_emulation` function for future use case such as stopping the VM or triggering a gdbstub exception

 contrib/plugins/Makefile      |   1 +
 contrib/plugins/stoptrigger.c | 150 ++++++++++++++++++++++++++++++++++
 docs/devel/tcg-plugins.rst    |  22 +++++
 3 files changed, 173 insertions(+)
 create mode 100644 contrib/plugins/stoptrigger.c

Comments

Alex Bennée July 11, 2024, 10:03 a.m. UTC | #1
Simon Hamelin <simon.hamelin@grenoble-inp.org> writes:

> This new plugin allows to stop emulation using conditions on the
> emulation state. By setting this plugin arguments, it is possible
> to set an instruction count limit and/or trigger address(es) to stop at.
> The code returned at emulation exit can be customized.
>
> This plugin demonstrates how someone could stop QEMU execution.
> It could be used for research purposes to launch some code and
> deterministically stop it and understand where its execution flow went.
>
> Co-authored-by: Alexandre Iooss <erdnaxe@crans.org>
> Signed-off-by: Simon Hamelin <simon.hamelin@grenoble-inp.org>
> Signed-off-by: Alexandre Iooss <erdnaxe@crans.org>
> ---
> v2:
>   - use a scoreboard for counting instructions
>   - no longer hook each instruction to exit at given address
>   - add `exit_emulation` function for future use case such as stopping the VM or triggering a gdbstub exception
>
>  contrib/plugins/Makefile      |   1 +
>  contrib/plugins/stoptrigger.c | 150 ++++++++++++++++++++++++++++++++++
>  docs/devel/tcg-plugins.rst    |  22 +++++
>  3 files changed, 173 insertions(+)
>  create mode 100644 contrib/plugins/stoptrigger.c
>
> diff --git a/contrib/plugins/Makefile b/contrib/plugins/Makefile
> index 449ead1130..98a89d5c40 100644
> --- a/contrib/plugins/Makefile
> +++ b/contrib/plugins/Makefile
> @@ -28,6 +28,7 @@ NAMES += hwprofile
>  NAMES += cache
>  NAMES += drcov
>  NAMES += ips
> +NAMES += stoptrigger
>  
>  ifeq ($(CONFIG_WIN32),y)
>  SO_SUFFIX := .dll
> diff --git a/contrib/plugins/stoptrigger.c b/contrib/plugins/stoptrigger.c
> new file mode 100644
> index 0000000000..9c23f5854e
> --- /dev/null
> +++ b/contrib/plugins/stoptrigger.c
> @@ -0,0 +1,150 @@
> +/*
> + * Copyright (C) 2024, Simon Hamelin <simon.hamelin@grenoble-inp.org>
> + *
> + * Stop execution once a given address is reached or if the
> + * count of executed instructions reached a specified limit
> + *
> + * License: GNU GPL, version 2 or later.
> + *   See the COPYING file in the top-level directory.
> + */
> +

you are missing a glib.h include here

> +#include <assert.h>
> +#include <inttypes.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +
> +#include <qemu-plugin.h>
> +
> +QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION;
> +
> +/* Scoreboard to track executed instructions count */
> +typedef struct {
> +    uint64_t insn_count;
> +} InstructionsCount;
> +static struct qemu_plugin_scoreboard *insn_count_sb;
> +static qemu_plugin_u64 insn_count;
> +
> +static uint64_t icount;
> +static int icount_exit_code;
> +
> +static bool exit_on_icount;
> +static bool exit_on_address;
> +
> +/* Map trigger addresses to exit code */
> +static GHashTable *addrs_ht;
> +static GMutex addrs_ht_lock;
> +
> +static void exit_emulation(int return_code)
> +{
> +    exit(return_code);
> +}
> +
> +static void exit_icount_reached(unsigned int cpu_index, void *udata)
> +{
> +    qemu_plugin_outs("icount reached, exiting\n");
> +    exit_emulation(icount_exit_code);
> +}
> +
> +static void exit_address_reached(unsigned int cpu_index, void *udata)
> +{
> +    uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
> +    g_mutex_lock(&addrs_ht_lock);
> +    int exit_code = GPOINTER_TO_INT(
> +        g_hash_table_lookup(addrs_ht, GUINT_TO_POINTER(insn_vaddr)));
> +    g_mutex_unlock(&addrs_ht_lock);
> +    char *msg = g_strdup_printf("0x%" PRIx64 " reached, exiting\n",
> insn_vaddr);

Dont intermix variable declarations, put them at the top of the block.

> +    qemu_plugin_outs(msg);
> +    exit_emulation(exit_code);
> +}

How about something like:

  static void exit_emulation(int return_code, char *message)
  {
      qemu_plugin_outs(message);
      g_free(message);
      exit(return_code);
  }

  static void exit_icount_reached(unsigned int cpu_index, void *udata)
  {
      uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
      char *msg = g_strdup_printf("icount reached at 0x%" PRIx64 ", exiting\n", insn_vaddr);

      exit_emulation(icount_exit_code, msg);
  }

  static void exit_address_reached(unsigned int cpu_index, void *udata)
  {
      uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
      char *msg = g_strdup_printf("0x%" PRIx64 " reached, exiting\n", insn_vaddr);
      int exit_code;

      g_mutex_lock(&addrs_ht_lock);
      exit_code = GPOINTER_TO_INT(
          g_hash_table_lookup(addrs_ht, GUINT_TO_POINTER(insn_vaddr)));
      g_mutex_unlock(&addrs_ht_lock);

      exit_emulation(exit_code, msg);
  }


> +
> +static void vcpu_tb_trans(qemu_plugin_id_t id, struct qemu_plugin_tb *tb)
> +{
> +    size_t tb_n = qemu_plugin_tb_n_insns(tb);
> +    for (size_t i = 0; i < tb_n; i++) {
> +        struct qemu_plugin_insn *insn = qemu_plugin_tb_get_insn(tb, i);
> +        uint64_t insn_vaddr = qemu_plugin_insn_vaddr(insn);

might as well just cast it once:

        gpointer insn_vaddr = GUINT_TO_POINTER(qemu_plugin_insn_vaddr(insn));


> +
> +        if (exit_on_icount) {
> +            /* Increment and check scoreboard for each instruction */
> +            qemu_plugin_register_vcpu_insn_exec_inline_per_vcpu(
> +                insn, QEMU_PLUGIN_INLINE_ADD_U64, insn_count, 1);
> +            qemu_plugin_register_vcpu_insn_exec_cond_cb(
> +                insn, exit_icount_reached, QEMU_PLUGIN_CB_NO_REGS,
> +                QEMU_PLUGIN_COND_EQ, insn_count, icount + 1, NULL);

Might be useful to report the PC at which you reach the icount boundary

> +        }
> +
> +        if (exit_on_address) {
> +            g_mutex_lock(&addrs_ht_lock);
> +            if (g_hash_table_contains(addrs_ht, GUINT_TO_POINTER(insn_vaddr))) {
> +                /* Exit triggered by address */
> +                qemu_plugin_register_vcpu_insn_exec_cb(
> +                    insn, exit_address_reached, QEMU_PLUGIN_CB_NO_REGS,
> +                    GUINT_TO_POINTER(insn_vaddr));
> +            }
> +            g_mutex_unlock(&addrs_ht_lock);
> +        }
> +    }
> +}
> +
> +static void plugin_exit(qemu_plugin_id_t id, void *p)
> +{
> +    g_hash_table_destroy(addrs_ht);
> +    qemu_plugin_scoreboard_free(insn_count_sb);
> +}
> +
> +QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
> +                                           const qemu_info_t *info, int argc,
> +                                           char **argv)
> +{
> +    addrs_ht = g_hash_table_new(NULL, g_direct_equal);
> +
> +    insn_count_sb = qemu_plugin_scoreboard_new(sizeof(InstructionsCount));
> +    insn_count = qemu_plugin_scoreboard_u64_in_struct(
> +        insn_count_sb, InstructionsCount, insn_count);
> +
> +    for (int i = 0; i < argc; i++) {
> +        char *opt = argv[i];
> +        g_auto(GStrv) tokens = g_strsplit(opt, "=", 2);
> +        if (g_strcmp0(tokens[0], "icount") == 0) {
> +            g_auto(GStrv) icount_tokens = g_strsplit(tokens[1], ":", 2);
> +            icount = g_ascii_strtoull(icount_tokens[0], NULL, 0);



> +            if (icount < 1 || g_strrstr(icount_tokens[0], "-") !=
> NULL) {

I don't think strstoull would even parse something with - in it so I
would just do:

  if (icount == 0) {
     /* fail */
  }

> +                fprintf(stderr,
> +                        "icount parsing failed: '%s' must be a positive "
> +                        "integer\n",
> +                        icount_tokens[0]);
> +                return -1;
> +            }
> +            if (icount_tokens[1]) {
> +                icount_exit_code = g_ascii_strtoull(icount_tokens[1], NULL, 0);
> +            }
> +            exit_on_icount = true;
> +        } else if (g_strcmp0(tokens[0], "addr") == 0) {
> +            g_auto(GStrv) addr_tokens = g_strsplit(tokens[1], ":", 2);
> +            uint64_t exit_addr = g_ascii_strtoull(addr_tokens[0], NULL, 0);
> +            int exit_code = 0;
> +            if (addr_tokens[1]) {
> +                exit_code = g_ascii_strtoull(addr_tokens[1], NULL, 0);
> +            }
> +            g_mutex_lock(&addrs_ht_lock);
> +            g_hash_table_insert(addrs_ht, GUINT_TO_POINTER(exit_addr),
> +                                GINT_TO_POINTER(exit_code));
> +            g_mutex_unlock(&addrs_ht_lock);
> +            exit_on_address = true;
> +        } else {
> +            fprintf(stderr, "option parsing failed: %s\n", opt);
> +            return -1;
> +        }
> +    }
> +
> +    if (!exit_on_icount && !exit_on_address) {
> +        fprintf(stderr, "'icount' or 'addr' argument missing\n");
> +        return -1;
> +    }
> +
> +    /* Register translation block and exit callbacks */
> +    qemu_plugin_register_vcpu_tb_trans_cb(id, vcpu_tb_trans);
> +    qemu_plugin_register_atexit_cb(id, plugin_exit, NULL);
> +
> +    return 0;
> +}
> diff --git a/docs/devel/tcg-plugins.rst b/docs/devel/tcg-plugins.rst
> index f7d7b9e3a4..954623f9bf 100644
> --- a/docs/devel/tcg-plugins.rst
> +++ b/docs/devel/tcg-plugins.rst
> @@ -642,6 +642,28 @@ The plugin has a number of arguments, all of them are optional:
>    configuration arguments implies ``l2=on``.
>    (default: N = 2097152 (2MB), B = 64, A = 16)
>  
> +- contrib/plugins/stoptrigger.c
> +
> +The stoptrigger plugin allows to setup triggers to stop emulation.
> +It can be used for research purposes to launch some code and precisely stop it
> +and understand where its execution flow went.
> +
> +Two types of triggers can be configured: a count of instructions to stop at,
> +or an address to stop at. Multiple triggers can be set at once.
> +
> +By default, QEMU will exit with return code 0. A custom return code can be
> +configured for each trigger using ``:CODE`` syntax.
> +
> +For example, to stop at the 20-th instruction with return code 41, at address
> +0xd4 with return code 0 or at address 0xd8 with return code 42::
> +
> +  $ qemu-system-aarch64 $(QEMU_ARGS) \
> +    -plugin ./contrib/plugins/libstoptrigger.so,icount=20:41,addr=0xd4,addr=0xd8:42 -d plugin
> +
> +The plugin will log the reason of exit, for example::
> +
> +  0xd4 reached, exiting
> +
>  Plugin API
>  ==========

Otherwise it looks good to me. Unless you want to tackle additional exit
modes?

What is your current use case for this?
Simon Hamelin July 12, 2024, 7:53 a.m. UTC | #2
On 7/11/24 12:03, Alex Bennée wrote:
>> +static void exit_emulation(int return_code)
>> +{
>> +    exit(return_code);
>> +}
>> +
>> +static void exit_icount_reached(unsigned int cpu_index, void *udata)
>> +{
>> +    qemu_plugin_outs("icount reached, exiting\n");
>> +    exit_emulation(icount_exit_code);
>> +}
>> +
>> +static void exit_address_reached(unsigned int cpu_index, void *udata)
>> +{
>> +    uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
>> +    g_mutex_lock(&addrs_ht_lock);
>> +    int exit_code = GPOINTER_TO_INT(
>> +        g_hash_table_lookup(addrs_ht, GUINT_TO_POINTER(insn_vaddr)));
>> +    g_mutex_unlock(&addrs_ht_lock);
>> +    char *msg = g_strdup_printf("0x%" PRIx64 " reached, exiting\n",
>> insn_vaddr);
> 
> Dont intermix variable declarations, put them at the top of the block.
> 
>> +    qemu_plugin_outs(msg);
>> +    exit_emulation(exit_code);
>> +}
> 
> How about something like:
> 
>    static void exit_emulation(int return_code, char *message)
>    {
>        qemu_plugin_outs(message);
>        g_free(message);
>        exit(return_code);
>    }
> 
>    static void exit_icount_reached(unsigned int cpu_index, void *udata)
>    {
>        uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
>        char *msg = g_strdup_printf("icount reached at 0x%" PRIx64 ", exiting\n", insn_vaddr);
> 
>        exit_emulation(icount_exit_code, msg);
>    }
> 
>    static void exit_address_reached(unsigned int cpu_index, void *udata)
>    {
>        uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
>        char *msg = g_strdup_printf("0x%" PRIx64 " reached, exiting\n", insn_vaddr);
>        int exit_code;
> 
>        g_mutex_lock(&addrs_ht_lock);
>        exit_code = GPOINTER_TO_INT(
>            g_hash_table_lookup(addrs_ht, GUINT_TO_POINTER(insn_vaddr)));
>        g_mutex_unlock(&addrs_ht_lock);
> 
>        exit_emulation(exit_code, msg);
>    }
> 
> 

Looks good to me, will definitly put that in the next patch !

>> +QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
>> +                                           const qemu_info_t *info, int argc,
>> +                                           char **argv)
>> +{
>> +    addrs_ht = g_hash_table_new(NULL, g_direct_equal);
>> +
>> +    insn_count_sb = qemu_plugin_scoreboard_new(sizeof(InstructionsCount));
>> +    insn_count = qemu_plugin_scoreboard_u64_in_struct(
>> +        insn_count_sb, InstructionsCount, insn_count);
>> +
>> +    for (int i = 0; i < argc; i++) {
>> +        char *opt = argv[i];
>> +        g_auto(GStrv) tokens = g_strsplit(opt, "=", 2);
>> +        if (g_strcmp0(tokens[0], "icount") == 0) {
>> +            g_auto(GStrv) icount_tokens = g_strsplit(tokens[1], ":", 2);
>> +            icount = g_ascii_strtoull(icount_tokens[0], NULL, 0);
> 
> 
> 
>> +            if (icount < 1 || g_strrstr(icount_tokens[0], "-") !=
>> NULL) {
> 
> I don't think strstoull would even parse something with - in it so I
> would just do:
> 
>    if (icount == 0) {
>       /* fail */
>    }
> 

According to the GLib documentation: "Note that input with a leading 
minus sign (-) is accepted, and will return the negation of the parsed 
number, unless that would overflow a guint64". So i guess we need to 
keep this check.

>> diff --git a/docs/devel/tcg-plugins.rst b/docs/devel/tcg-plugins.rst
>> index f7d7b9e3a4..954623f9bf 100644
>> --- a/docs/devel/tcg-plugins.rst
>> +++ b/docs/devel/tcg-plugins.rst
>> @@ -642,6 +642,28 @@ The plugin has a number of arguments, all of them are optional:
>>     configuration arguments implies ``l2=on``.
>>     (default: N = 2097152 (2MB), B = 64, A = 16)
>>   
>> +- contrib/plugins/stoptrigger.c
>> +
>> +The stoptrigger plugin allows to setup triggers to stop emulation.
>> +It can be used for research purposes to launch some code and precisely stop it
>> +and understand where its execution flow went.
>> +
>> +Two types of triggers can be configured: a count of instructions to stop at,
>> +or an address to stop at. Multiple triggers can be set at once.
>> +
>> +By default, QEMU will exit with return code 0. A custom return code can be
>> +configured for each trigger using ``:CODE`` syntax.
>> +
>> +For example, to stop at the 20-th instruction with return code 41, at address
>> +0xd4 with return code 0 or at address 0xd8 with return code 42::
>> +
>> +  $ qemu-system-aarch64 $(QEMU_ARGS) \
>> +    -plugin ./contrib/plugins/libstoptrigger.so,icount=20:41,addr=0xd4,addr=0xd8:42 -d plugin
>> +
>> +The plugin will log the reason of exit, for example::
>> +
>> +  0xd4 reached, exiting
>> +
>>   Plugin API
>>   ==========
> 
> Otherwise it looks good to me. Unless you want to tackle additional exit
> modes?
> 
> What is your current use case for this?
>

I'm currently using this plugin to determine where my programm stop 
after a given number of instructions executed.
Pierrick Bouvier July 12, 2024, 5:23 p.m. UTC | #3
Hello Simon,

On 7/12/24 00:53, Simon Hamelin wrote:
> 
> 
> On 7/11/24 12:03, Alex Bennée wrote:
>>> +static void exit_emulation(int return_code)
>>> +{
>>> +    exit(return_code);
>>> +}
>>> +
>>> +static void exit_icount_reached(unsigned int cpu_index, void *udata)
>>> +{
>>> +    qemu_plugin_outs("icount reached, exiting\n");
>>> +    exit_emulation(icount_exit_code);
>>> +}
>>> +
>>> +static void exit_address_reached(unsigned int cpu_index, void *udata)
>>> +{
>>> +    uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
>>> +    g_mutex_lock(&addrs_ht_lock);
>>> +    int exit_code = GPOINTER_TO_INT(
>>> +        g_hash_table_lookup(addrs_ht, GUINT_TO_POINTER(insn_vaddr)));
>>> +    g_mutex_unlock(&addrs_ht_lock);
>>> +    char *msg = g_strdup_printf("0x%" PRIx64 " reached, exiting\n",
>>> insn_vaddr);
>>
>> Dont intermix variable declarations, put them at the top of the block.
>>
>>> +    qemu_plugin_outs(msg);
>>> +    exit_emulation(exit_code);
>>> +}
>>
>> How about something like:
>>
>>     static void exit_emulation(int return_code, char *message)
>>     {
>>         qemu_plugin_outs(message);
>>         g_free(message);
>>         exit(return_code);
>>     }
>>
>>     static void exit_icount_reached(unsigned int cpu_index, void *udata)
>>     {
>>         uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
>>         char *msg = g_strdup_printf("icount reached at 0x%" PRIx64 ", exiting\n", insn_vaddr);
>>
>>         exit_emulation(icount_exit_code, msg);
>>     }
>>
>>     static void exit_address_reached(unsigned int cpu_index, void *udata)
>>     {
>>         uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
>>         char *msg = g_strdup_printf("0x%" PRIx64 " reached, exiting\n", insn_vaddr);
>>         int exit_code;
>>
>>         g_mutex_lock(&addrs_ht_lock);
>>         exit_code = GPOINTER_TO_INT(
>>             g_hash_table_lookup(addrs_ht, GUINT_TO_POINTER(insn_vaddr)));
>>         g_mutex_unlock(&addrs_ht_lock);
>>
>>         exit_emulation(exit_code, msg);
>>     }
>>
>>
> 
> Looks good to me, will definitly put that in the next patch !
> 
>>> +QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
>>> +                                           const qemu_info_t *info, int argc,
>>> +                                           char **argv)
>>> +{
>>> +    addrs_ht = g_hash_table_new(NULL, g_direct_equal);
>>> +
>>> +    insn_count_sb = qemu_plugin_scoreboard_new(sizeof(InstructionsCount));
>>> +    insn_count = qemu_plugin_scoreboard_u64_in_struct(
>>> +        insn_count_sb, InstructionsCount, insn_count);
>>> +
>>> +    for (int i = 0; i < argc; i++) {
>>> +        char *opt = argv[i];
>>> +        g_auto(GStrv) tokens = g_strsplit(opt, "=", 2);
>>> +        if (g_strcmp0(tokens[0], "icount") == 0) {
>>> +            g_auto(GStrv) icount_tokens = g_strsplit(tokens[1], ":", 2);
>>> +            icount = g_ascii_strtoull(icount_tokens[0], NULL, 0);
>>
>>
>>
>>> +            if (icount < 1 || g_strrstr(icount_tokens[0], "-") !=
>>> NULL) {
>>
>> I don't think strstoull would even parse something with - in it so I
>> would just do:
>>
>>     if (icount == 0) {
>>        /* fail */
>>     }
>>
> 
> According to the GLib documentation: "Note that input with a leading
> minus sign (-) is accepted, and will return the negation of the parsed
> number, unless that would overflow a guint64". So i guess we need to
> keep this check.
> 
>>> diff --git a/docs/devel/tcg-plugins.rst b/docs/devel/tcg-plugins.rst
>>> index f7d7b9e3a4..954623f9bf 100644
>>> --- a/docs/devel/tcg-plugins.rst
>>> +++ b/docs/devel/tcg-plugins.rst
>>> @@ -642,6 +642,28 @@ The plugin has a number of arguments, all of them are optional:
>>>      configuration arguments implies ``l2=on``.
>>>      (default: N = 2097152 (2MB), B = 64, A = 16)
>>>    
>>> +- contrib/plugins/stoptrigger.c
>>> +
>>> +The stoptrigger plugin allows to setup triggers to stop emulation.
>>> +It can be used for research purposes to launch some code and precisely stop it
>>> +and understand where its execution flow went.
>>> +
>>> +Two types of triggers can be configured: a count of instructions to stop at,
>>> +or an address to stop at. Multiple triggers can be set at once.
>>> +
>>> +By default, QEMU will exit with return code 0. A custom return code can be
>>> +configured for each trigger using ``:CODE`` syntax.
>>> +
>>> +For example, to stop at the 20-th instruction with return code 41, at address
>>> +0xd4 with return code 0 or at address 0xd8 with return code 42::
>>> +
>>> +  $ qemu-system-aarch64 $(QEMU_ARGS) \
>>> +    -plugin ./contrib/plugins/libstoptrigger.so,icount=20:41,addr=0xd4,addr=0xd8:42 -d plugin
>>> +
>>> +The plugin will log the reason of exit, for example::
>>> +
>>> +  0xd4 reached, exiting
>>> +
>>>    Plugin API
>>>    ==========
>>
>> Otherwise it looks good to me. Unless you want to tackle additional exit
>> modes?
>>
>> What is your current use case for this?
>>
> 
> I'm currently using this plugin to determine where my programm stop
> after a given number of instructions executed.
> 

Could you share a bit more information on the final goal, if possible?
Is that used for fuzzing binaries, security analysis, or other things?
Simon Hamelin July 15, 2024, 8:09 a.m. UTC | #4
Hello Pierrick,

On 7/12/24 19:23, Pierrick Bouvier wrote:
> Hello Simon,
> 
> On 7/12/24 00:53, Simon Hamelin wrote:
>>
>>
>> On 7/11/24 12:03, Alex Bennée wrote:
>>>> +static void exit_emulation(int return_code)
>>>> +{
>>>> +    exit(return_code);
>>>> +}
>>>> +
>>>> +static void exit_icount_reached(unsigned int cpu_index, void *udata)
>>>> +{
>>>> +    qemu_plugin_outs("icount reached, exiting\n");
>>>> +    exit_emulation(icount_exit_code);
>>>> +}
>>>> +
>>>> +static void exit_address_reached(unsigned int cpu_index, void *udata)
>>>> +{
>>>> +    uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
>>>> +    g_mutex_lock(&addrs_ht_lock);
>>>> +    int exit_code = GPOINTER_TO_INT(
>>>> +        g_hash_table_lookup(addrs_ht, GUINT_TO_POINTER(insn_vaddr)));
>>>> +    g_mutex_unlock(&addrs_ht_lock);
>>>> +    char *msg = g_strdup_printf("0x%" PRIx64 " reached, exiting\n",
>>>> insn_vaddr);
>>>
>>> Dont intermix variable declarations, put them at the top of the block.
>>>
>>>> +    qemu_plugin_outs(msg);
>>>> +    exit_emulation(exit_code);
>>>> +}
>>>
>>> How about something like:
>>>
>>>     static void exit_emulation(int return_code, char *message)
>>>     {
>>>         qemu_plugin_outs(message);
>>>         g_free(message);
>>>         exit(return_code);
>>>     }
>>>
>>>     static void exit_icount_reached(unsigned int cpu_index, void *udata)
>>>     {
>>>         uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
>>>         char *msg = g_strdup_printf("icount reached at 0x%" PRIx64 ", 
>>> exiting\n", insn_vaddr);
>>>
>>>         exit_emulation(icount_exit_code, msg);
>>>     }
>>>
>>>     static void exit_address_reached(unsigned int cpu_index, void 
>>> *udata)
>>>     {
>>>         uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
>>>         char *msg = g_strdup_printf("0x%" PRIx64 " reached, 
>>> exiting\n", insn_vaddr);
>>>         int exit_code;
>>>
>>>         g_mutex_lock(&addrs_ht_lock);
>>>         exit_code = GPOINTER_TO_INT(
>>>             g_hash_table_lookup(addrs_ht, 
>>> GUINT_TO_POINTER(insn_vaddr)));
>>>         g_mutex_unlock(&addrs_ht_lock);
>>>
>>>         exit_emulation(exit_code, msg);
>>>     }
>>>
>>>
>>
>> Looks good to me, will definitly put that in the next patch !
>>
>>>> +QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
>>>> +                                           const qemu_info_t *info, 
>>>> int argc,
>>>> +                                           char **argv)
>>>> +{
>>>> +    addrs_ht = g_hash_table_new(NULL, g_direct_equal);
>>>> +
>>>> +    insn_count_sb = 
>>>> qemu_plugin_scoreboard_new(sizeof(InstructionsCount));
>>>> +    insn_count = qemu_plugin_scoreboard_u64_in_struct(
>>>> +        insn_count_sb, InstructionsCount, insn_count);
>>>> +
>>>> +    for (int i = 0; i < argc; i++) {
>>>> +        char *opt = argv[i];
>>>> +        g_auto(GStrv) tokens = g_strsplit(opt, "=", 2);
>>>> +        if (g_strcmp0(tokens[0], "icount") == 0) {
>>>> +            g_auto(GStrv) icount_tokens = g_strsplit(tokens[1], 
>>>> ":", 2);
>>>> +            icount = g_ascii_strtoull(icount_tokens[0], NULL, 0);
>>>
>>>
>>>
>>>> +            if (icount < 1 || g_strrstr(icount_tokens[0], "-") !=
>>>> NULL) {
>>>
>>> I don't think strstoull would even parse something with - in it so I
>>> would just do:
>>>
>>>     if (icount == 0) {
>>>        /* fail */
>>>     }
>>>
>>
>> According to the GLib documentation: "Note that input with a leading
>> minus sign (-) is accepted, and will return the negation of the parsed
>> number, unless that would overflow a guint64". So i guess we need to
>> keep this check.
>>
>>>> diff --git a/docs/devel/tcg-plugins.rst b/docs/devel/tcg-plugins.rst
>>>> index f7d7b9e3a4..954623f9bf 100644
>>>> --- a/docs/devel/tcg-plugins.rst
>>>> +++ b/docs/devel/tcg-plugins.rst
>>>> @@ -642,6 +642,28 @@ The plugin has a number of arguments, all of 
>>>> them are optional:
>>>>      configuration arguments implies ``l2=on``.
>>>>      (default: N = 2097152 (2MB), B = 64, A = 16)
>>>> +- contrib/plugins/stoptrigger.c
>>>> +
>>>> +The stoptrigger plugin allows to setup triggers to stop emulation.
>>>> +It can be used for research purposes to launch some code and 
>>>> precisely stop it
>>>> +and understand where its execution flow went.
>>>> +
>>>> +Two types of triggers can be configured: a count of instructions to 
>>>> stop at,
>>>> +or an address to stop at. Multiple triggers can be set at once.
>>>> +
>>>> +By default, QEMU will exit with return code 0. A custom return code 
>>>> can be
>>>> +configured for each trigger using ``:CODE`` syntax.
>>>> +
>>>> +For example, to stop at the 20-th instruction with return code 41, 
>>>> at address
>>>> +0xd4 with return code 0 or at address 0xd8 with return code 42::
>>>> +
>>>> +  $ qemu-system-aarch64 $(QEMU_ARGS) \
>>>> +    -plugin 
>>>> ./contrib/plugins/libstoptrigger.so,icount=20:41,addr=0xd4,addr=0xd8:42 -d plugin
>>>> +
>>>> +The plugin will log the reason of exit, for example::
>>>> +
>>>> +  0xd4 reached, exiting
>>>> +
>>>>    Plugin API
>>>>    ==========
>>>
>>> Otherwise it looks good to me. Unless you want to tackle additional exit
>>> modes?
>>>
>>> What is your current use case for this?
>>>
>>
>> I'm currently using this plugin to determine where my programm stop
>> after a given number of instructions executed.
>>
> 
> Could you share a bit more information on the final goal, if possible?
> Is that used for fuzzing binaries, security analysis, or other things?

I'm currently using this plugin for security analysis purposes. 
Basically my goal is to simulate fault injection using QEMU. To do so 
I'm using this plugin along with another plugin that skips an 
instruction at a given address. With this plugin I'm able to see how the 
program reacts to the fault and stop it with a custom return code. 
Basically there are 4 cases:
    - The fault does not disrupt the program and it reaches the expected 
address.
    - The fault disrupt the program and it reaches an unexpected address 
known as the "target" address.
    - The fault disrupt the program in such a way that it executes a lot 
of instructions without reaching the expected address, this situation is 
known as a "timeout".
    - The fault crashes the guest, causing it to reach an error handler 
address.

In short, here's a command line that illustrates how I use the plugin:

$ qemu-system-aarch64 $(QEMU_ARGS) -plugin 
./contrib/plugins/libstoptrigger.so,icount=timeout_count,addr=addr=expected_addr:0,error_handler_addr:1,target_addr:2 
-d plugin

--
Simon Hamelin
Simon Hamelin July 15, 2024, 8:25 a.m. UTC | #5
On 7/15/24 10:09, Simon Hamelin wrote:
> Hello Pierrick,
> 
> On 7/12/24 19:23, Pierrick Bouvier wrote:
>> Hello Simon,
>>
>> On 7/12/24 00:53, Simon Hamelin wrote:
>>>
>>>
>>> On 7/11/24 12:03, Alex Bennée wrote:
>>>>> +static void exit_emulation(int return_code)
>>>>> +{
>>>>> +    exit(return_code);
>>>>> +}
>>>>> +
>>>>> +static void exit_icount_reached(unsigned int cpu_index, void *udata)
>>>>> +{
>>>>> +    qemu_plugin_outs("icount reached, exiting\n");
>>>>> +    exit_emulation(icount_exit_code);
>>>>> +}
>>>>> +
>>>>> +static void exit_address_reached(unsigned int cpu_index, void *udata)
>>>>> +{
>>>>> +    uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
>>>>> +    g_mutex_lock(&addrs_ht_lock);
>>>>> +    int exit_code = GPOINTER_TO_INT(
>>>>> +        g_hash_table_lookup(addrs_ht, GUINT_TO_POINTER(insn_vaddr)));
>>>>> +    g_mutex_unlock(&addrs_ht_lock);
>>>>> +    char *msg = g_strdup_printf("0x%" PRIx64 " reached, exiting\n",
>>>>> insn_vaddr);
>>>>
>>>> Dont intermix variable declarations, put them at the top of the block.
>>>>
>>>>> +    qemu_plugin_outs(msg);
>>>>> +    exit_emulation(exit_code);
>>>>> +}
>>>>
>>>> How about something like:
>>>>
>>>>     static void exit_emulation(int return_code, char *message)
>>>>     {
>>>>         qemu_plugin_outs(message);
>>>>         g_free(message);
>>>>         exit(return_code);
>>>>     }
>>>>
>>>>     static void exit_icount_reached(unsigned int cpu_index, void 
>>>> *udata)
>>>>     {
>>>>         uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
>>>>         char *msg = g_strdup_printf("icount reached at 0x%" PRIx64 
>>>> ", exiting\n", insn_vaddr);
>>>>
>>>>         exit_emulation(icount_exit_code, msg);
>>>>     }
>>>>
>>>>     static void exit_address_reached(unsigned int cpu_index, void 
>>>> *udata)
>>>>     {
>>>>         uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
>>>>         char *msg = g_strdup_printf("0x%" PRIx64 " reached, 
>>>> exiting\n", insn_vaddr);
>>>>         int exit_code;
>>>>
>>>>         g_mutex_lock(&addrs_ht_lock);
>>>>         exit_code = GPOINTER_TO_INT(
>>>>             g_hash_table_lookup(addrs_ht, 
>>>> GUINT_TO_POINTER(insn_vaddr)));
>>>>         g_mutex_unlock(&addrs_ht_lock);
>>>>
>>>>         exit_emulation(exit_code, msg);
>>>>     }
>>>>
>>>>
>>>
>>> Looks good to me, will definitly put that in the next patch !
>>>
>>>>> +QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
>>>>> +                                           const qemu_info_t 
>>>>> *info, int argc,
>>>>> +                                           char **argv)
>>>>> +{
>>>>> +    addrs_ht = g_hash_table_new(NULL, g_direct_equal);
>>>>> +
>>>>> +    insn_count_sb = 
>>>>> qemu_plugin_scoreboard_new(sizeof(InstructionsCount));
>>>>> +    insn_count = qemu_plugin_scoreboard_u64_in_struct(
>>>>> +        insn_count_sb, InstructionsCount, insn_count);
>>>>> +
>>>>> +    for (int i = 0; i < argc; i++) {
>>>>> +        char *opt = argv[i];
>>>>> +        g_auto(GStrv) tokens = g_strsplit(opt, "=", 2);
>>>>> +        if (g_strcmp0(tokens[0], "icount") == 0) {
>>>>> +            g_auto(GStrv) icount_tokens = g_strsplit(tokens[1], 
>>>>> ":", 2);
>>>>> +            icount = g_ascii_strtoull(icount_tokens[0], NULL, 0);
>>>>
>>>>
>>>>
>>>>> +            if (icount < 1 || g_strrstr(icount_tokens[0], "-") !=
>>>>> NULL) {
>>>>
>>>> I don't think strstoull would even parse something with - in it so I
>>>> would just do:
>>>>
>>>>     if (icount == 0) {
>>>>        /* fail */
>>>>     }
>>>>
>>>
>>> According to the GLib documentation: "Note that input with a leading
>>> minus sign (-) is accepted, and will return the negation of the parsed
>>> number, unless that would overflow a guint64". So i guess we need to
>>> keep this check.
>>>
>>>>> diff --git a/docs/devel/tcg-plugins.rst b/docs/devel/tcg-plugins.rst
>>>>> index f7d7b9e3a4..954623f9bf 100644
>>>>> --- a/docs/devel/tcg-plugins.rst
>>>>> +++ b/docs/devel/tcg-plugins.rst
>>>>> @@ -642,6 +642,28 @@ The plugin has a number of arguments, all of 
>>>>> them are optional:
>>>>>      configuration arguments implies ``l2=on``.
>>>>>      (default: N = 2097152 (2MB), B = 64, A = 16)
>>>>> +- contrib/plugins/stoptrigger.c
>>>>> +
>>>>> +The stoptrigger plugin allows to setup triggers to stop emulation.
>>>>> +It can be used for research purposes to launch some code and 
>>>>> precisely stop it
>>>>> +and understand where its execution flow went.
>>>>> +
>>>>> +Two types of triggers can be configured: a count of instructions 
>>>>> to stop at,
>>>>> +or an address to stop at. Multiple triggers can be set at once.
>>>>> +
>>>>> +By default, QEMU will exit with return code 0. A custom return 
>>>>> code can be
>>>>> +configured for each trigger using ``:CODE`` syntax.
>>>>> +
>>>>> +For example, to stop at the 20-th instruction with return code 41, 
>>>>> at address
>>>>> +0xd4 with return code 0 or at address 0xd8 with return code 42::
>>>>> +
>>>>> +  $ qemu-system-aarch64 $(QEMU_ARGS) \
>>>>> +    -plugin 
>>>>> ./contrib/plugins/libstoptrigger.so,icount=20:41,addr=0xd4,addr=0xd8:42 -d plugin
>>>>> +
>>>>> +The plugin will log the reason of exit, for example::
>>>>> +
>>>>> +  0xd4 reached, exiting
>>>>> +
>>>>>    Plugin API
>>>>>    ==========
>>>>
>>>> Otherwise it looks good to me. Unless you want to tackle additional 
>>>> exit
>>>> modes?
>>>>
>>>> What is your current use case for this?
>>>>
>>>
>>> I'm currently using this plugin to determine where my programm stop
>>> after a given number of instructions executed.
>>>
>>
>> Could you share a bit more information on the final goal, if possible?
>> Is that used for fuzzing binaries, security analysis, or other things?
> 
> I'm currently using this plugin for security analysis purposes. 
> Basically my goal is to simulate fault injection using QEMU. To do so 
> I'm using this plugin along with another plugin that skips an 
> instruction at a given address. With this plugin I'm able to see how the 
> program reacts to the fault and stop it with a custom return code. 
> Basically there are 4 cases:
>     - The fault does not disrupt the program and it reaches the expected 
> address.
>     - The fault disrupt the program and it reaches an unexpected address 
> known as the "target" address.
>     - The fault disrupt the program in such a way that it executes a lot 
> of instructions without reaching the expected address, this situation is 
> known as a "timeout".
>     - The fault crashes the guest, causing it to reach an error handler 
> address.
> 
> In short, here's a command line that illustrates how I use the plugin:
> 
> $ qemu-system-aarch64 $(QEMU_ARGS) -plugin 
> ./contrib/plugins/libstoptrigger.so,icount=timeout_count,addr=addr=expected_addr:0,error_handler_addr:1,target_addr:2 -d plugin
> 
> -- 
> Simon Hamelin

Look like I made a little mistake in the command line example, here is 
the correct one:

qemu-system-aarch64 $(QEMU_ARGS) -plugin 
./contrib/plugins/libstoptrigger.so,icount=timeout_count,addr=expected_addr:0,addr=error_handler_addr:1,addr=target_addr:2 
-d plugin

--
Simon Hamelin
Pierrick Bouvier July 15, 2024, 11:08 p.m. UTC | #6
On 7/15/24 01:09, Simon Hamelin wrote:
> Hello Pierrick,
> 
>> Could you share a bit more information on the final goal, if possible?
>> Is that used for fuzzing binaries, security analysis, or other things?
> 
> I'm currently using this plugin for security analysis purposes.
> Basically my goal is to simulate fault injection using QEMU. To do so
> I'm using this plugin along with another plugin that skips an
> instruction at a given address. With this plugin I'm able to see how the
> program reacts to the fault and stop it with a custom return code.
> Basically there are 4 cases:
>      - The fault does not disrupt the program and it reaches the expected
> address.
>      - The fault disrupt the program and it reaches an unexpected address
> known as the "target" address.
>      - The fault disrupt the program in such a way that it executes a lot
> of instructions without reaching the expected address, this situation is
> known as a "timeout".
>      - The fault crashes the guest, causing it to reach an error handler
> address.
> 

Thanks for sharing this, it's an interesting use case.

How did you implement instruction skipping?
Is that based on current API, or do you have a QEMU fork?

> In short, here's a command line that illustrates how I use the plugin:
> 
> $ qemu-system-aarch64 $(QEMU_ARGS) -plugin
> ./contrib/plugins/libstoptrigger.so,icount=timeout_count,addr=addr=expected_addr:0,error_handler_addr:1,target_addr:2
> -d plugin
> 
> --
> Simon Hamelin
Simon Hamelin July 16, 2024, 9:02 a.m. UTC | #7
On 7/16/24 01:08, Pierrick Bouvier wrote:
> On 7/15/24 01:09, Simon Hamelin wrote:
>> Hello Pierrick,
>>
>>> Could you share a bit more information on the final goal, if possible?
>>> Is that used for fuzzing binaries, security analysis, or other things?
>>
>> I'm currently using this plugin for security analysis purposes.
>> Basically my goal is to simulate fault injection using QEMU. To do so
>> I'm using this plugin along with another plugin that skips an
>> instruction at a given address. With this plugin I'm able to see how the
>> program reacts to the fault and stop it with a custom return code.
>> Basically there are 4 cases:
>>      - The fault does not disrupt the program and it reaches the expected
>> address.
>>      - The fault disrupt the program and it reaches an unexpected address
>> known as the "target" address.
>>      - The fault disrupt the program in such a way that it executes a lot
>> of instructions without reaching the expected address, this situation is
>> known as a "timeout".
>>      - The fault crashes the guest, causing it to reach an error handler
>> address.
>>
> 
> Thanks for sharing this, it's an interesting use case.
> 
> How did you implement instruction skipping?
> Is that based on current API, or do you have a QEMU fork?
> 

I had to extend the current API to support writing guest registers, 
specifically I'm skipping instructions by writing to the PC register. To 
do this I use the set_pc function of the CPUClass struct. This requires 
a bit of a hack to make the TCG react to the PC change as it's normal 
behavior is to execute the entire TB and ignore the PC change. To make 
the TCG react to this modification the only way I've found is to leave 
it midway it's execution using the `cpu_loop_exit` function.
Pierrick Bouvier July 16, 2024, 3:35 p.m. UTC | #8
On 7/16/24 02:02, Simon Hamelin wrote:
> 
> 
> On 7/16/24 01:08, Pierrick Bouvier wrote:
>> On 7/15/24 01:09, Simon Hamelin wrote:
>>> Hello Pierrick,
>>>
>>>> Could you share a bit more information on the final goal, if possible?
>>>> Is that used for fuzzing binaries, security analysis, or other things?
>>>
>>> I'm currently using this plugin for security analysis purposes.
>>> Basically my goal is to simulate fault injection using QEMU. To do so
>>> I'm using this plugin along with another plugin that skips an
>>> instruction at a given address. With this plugin I'm able to see how the
>>> program reacts to the fault and stop it with a custom return code.
>>> Basically there are 4 cases:
>>>       - The fault does not disrupt the program and it reaches the expected
>>> address.
>>>       - The fault disrupt the program and it reaches an unexpected address
>>> known as the "target" address.
>>>       - The fault disrupt the program in such a way that it executes a lot
>>> of instructions without reaching the expected address, this situation is
>>> known as a "timeout".
>>>       - The fault crashes the guest, causing it to reach an error handler
>>> address.
>>>
>>
>> Thanks for sharing this, it's an interesting use case.
>>
>> How did you implement instruction skipping?
>> Is that based on current API, or do you have a QEMU fork?
>>
> 
> I had to extend the current API to support writing guest registers,
> specifically I'm skipping instructions by writing to the PC register. To
> do this I use the set_pc function of the CPUClass struct. This requires
> a bit of a hack to make the TCG react to the PC change as it's normal
> behavior is to execute the entire TB and ignore the PC change. To make
> the TCG react to this modification the only way I've found is to leave
> it midway it's execution using the `cpu_loop_exit` function.
> 

Ok.
So far, our API is designed to not allow modification on guest side 
(code or data wise). It's definitely a discussion we should start at 
some point, as it may be needed for specific use cases.

Thanks!
diff mbox series

Patch

diff --git a/contrib/plugins/Makefile b/contrib/plugins/Makefile
index 449ead1130..98a89d5c40 100644
--- a/contrib/plugins/Makefile
+++ b/contrib/plugins/Makefile
@@ -28,6 +28,7 @@  NAMES += hwprofile
 NAMES += cache
 NAMES += drcov
 NAMES += ips
+NAMES += stoptrigger
 
 ifeq ($(CONFIG_WIN32),y)
 SO_SUFFIX := .dll
diff --git a/contrib/plugins/stoptrigger.c b/contrib/plugins/stoptrigger.c
new file mode 100644
index 0000000000..9c23f5854e
--- /dev/null
+++ b/contrib/plugins/stoptrigger.c
@@ -0,0 +1,150 @@ 
+/*
+ * Copyright (C) 2024, Simon Hamelin <simon.hamelin@grenoble-inp.org>
+ *
+ * Stop execution once a given address is reached or if the
+ * count of executed instructions reached a specified limit
+ *
+ * License: GNU GPL, version 2 or later.
+ *   See the COPYING file in the top-level directory.
+ */
+
+#include <assert.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <qemu-plugin.h>
+
+QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION;
+
+/* Scoreboard to track executed instructions count */
+typedef struct {
+    uint64_t insn_count;
+} InstructionsCount;
+static struct qemu_plugin_scoreboard *insn_count_sb;
+static qemu_plugin_u64 insn_count;
+
+static uint64_t icount;
+static int icount_exit_code;
+
+static bool exit_on_icount;
+static bool exit_on_address;
+
+/* Map trigger addresses to exit code */
+static GHashTable *addrs_ht;
+static GMutex addrs_ht_lock;
+
+static void exit_emulation(int return_code)
+{
+    exit(return_code);
+}
+
+static void exit_icount_reached(unsigned int cpu_index, void *udata)
+{
+    qemu_plugin_outs("icount reached, exiting\n");
+    exit_emulation(icount_exit_code);
+}
+
+static void exit_address_reached(unsigned int cpu_index, void *udata)
+{
+    uint64_t insn_vaddr = GPOINTER_TO_UINT(udata);
+    g_mutex_lock(&addrs_ht_lock);
+    int exit_code = GPOINTER_TO_INT(
+        g_hash_table_lookup(addrs_ht, GUINT_TO_POINTER(insn_vaddr)));
+    g_mutex_unlock(&addrs_ht_lock);
+    char *msg = g_strdup_printf("0x%" PRIx64 " reached, exiting\n", insn_vaddr);
+    qemu_plugin_outs(msg);
+    exit_emulation(exit_code);
+}
+
+static void vcpu_tb_trans(qemu_plugin_id_t id, struct qemu_plugin_tb *tb)
+{
+    size_t tb_n = qemu_plugin_tb_n_insns(tb);
+    for (size_t i = 0; i < tb_n; i++) {
+        struct qemu_plugin_insn *insn = qemu_plugin_tb_get_insn(tb, i);
+        uint64_t insn_vaddr = qemu_plugin_insn_vaddr(insn);
+
+        if (exit_on_icount) {
+            /* Increment and check scoreboard for each instruction */
+            qemu_plugin_register_vcpu_insn_exec_inline_per_vcpu(
+                insn, QEMU_PLUGIN_INLINE_ADD_U64, insn_count, 1);
+            qemu_plugin_register_vcpu_insn_exec_cond_cb(
+                insn, exit_icount_reached, QEMU_PLUGIN_CB_NO_REGS,
+                QEMU_PLUGIN_COND_EQ, insn_count, icount + 1, NULL);
+        }
+
+        if (exit_on_address) {
+            g_mutex_lock(&addrs_ht_lock);
+            if (g_hash_table_contains(addrs_ht, GUINT_TO_POINTER(insn_vaddr))) {
+                /* Exit triggered by address */
+                qemu_plugin_register_vcpu_insn_exec_cb(
+                    insn, exit_address_reached, QEMU_PLUGIN_CB_NO_REGS,
+                    GUINT_TO_POINTER(insn_vaddr));
+            }
+            g_mutex_unlock(&addrs_ht_lock);
+        }
+    }
+}
+
+static void plugin_exit(qemu_plugin_id_t id, void *p)
+{
+    g_hash_table_destroy(addrs_ht);
+    qemu_plugin_scoreboard_free(insn_count_sb);
+}
+
+QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
+                                           const qemu_info_t *info, int argc,
+                                           char **argv)
+{
+    addrs_ht = g_hash_table_new(NULL, g_direct_equal);
+
+    insn_count_sb = qemu_plugin_scoreboard_new(sizeof(InstructionsCount));
+    insn_count = qemu_plugin_scoreboard_u64_in_struct(
+        insn_count_sb, InstructionsCount, insn_count);
+
+    for (int i = 0; i < argc; i++) {
+        char *opt = argv[i];
+        g_auto(GStrv) tokens = g_strsplit(opt, "=", 2);
+        if (g_strcmp0(tokens[0], "icount") == 0) {
+            g_auto(GStrv) icount_tokens = g_strsplit(tokens[1], ":", 2);
+            icount = g_ascii_strtoull(icount_tokens[0], NULL, 0);
+            if (icount < 1 || g_strrstr(icount_tokens[0], "-") != NULL) {
+                fprintf(stderr,
+                        "icount parsing failed: '%s' must be a positive "
+                        "integer\n",
+                        icount_tokens[0]);
+                return -1;
+            }
+            if (icount_tokens[1]) {
+                icount_exit_code = g_ascii_strtoull(icount_tokens[1], NULL, 0);
+            }
+            exit_on_icount = true;
+        } else if (g_strcmp0(tokens[0], "addr") == 0) {
+            g_auto(GStrv) addr_tokens = g_strsplit(tokens[1], ":", 2);
+            uint64_t exit_addr = g_ascii_strtoull(addr_tokens[0], NULL, 0);
+            int exit_code = 0;
+            if (addr_tokens[1]) {
+                exit_code = g_ascii_strtoull(addr_tokens[1], NULL, 0);
+            }
+            g_mutex_lock(&addrs_ht_lock);
+            g_hash_table_insert(addrs_ht, GUINT_TO_POINTER(exit_addr),
+                                GINT_TO_POINTER(exit_code));
+            g_mutex_unlock(&addrs_ht_lock);
+            exit_on_address = true;
+        } else {
+            fprintf(stderr, "option parsing failed: %s\n", opt);
+            return -1;
+        }
+    }
+
+    if (!exit_on_icount && !exit_on_address) {
+        fprintf(stderr, "'icount' or 'addr' argument missing\n");
+        return -1;
+    }
+
+    /* Register translation block and exit callbacks */
+    qemu_plugin_register_vcpu_tb_trans_cb(id, vcpu_tb_trans);
+    qemu_plugin_register_atexit_cb(id, plugin_exit, NULL);
+
+    return 0;
+}
diff --git a/docs/devel/tcg-plugins.rst b/docs/devel/tcg-plugins.rst
index f7d7b9e3a4..954623f9bf 100644
--- a/docs/devel/tcg-plugins.rst
+++ b/docs/devel/tcg-plugins.rst
@@ -642,6 +642,28 @@  The plugin has a number of arguments, all of them are optional:
   configuration arguments implies ``l2=on``.
   (default: N = 2097152 (2MB), B = 64, A = 16)
 
+- contrib/plugins/stoptrigger.c
+
+The stoptrigger plugin allows to setup triggers to stop emulation.
+It can be used for research purposes to launch some code and precisely stop it
+and understand where its execution flow went.
+
+Two types of triggers can be configured: a count of instructions to stop at,
+or an address to stop at. Multiple triggers can be set at once.
+
+By default, QEMU will exit with return code 0. A custom return code can be
+configured for each trigger using ``:CODE`` syntax.
+
+For example, to stop at the 20-th instruction with return code 41, at address
+0xd4 with return code 0 or at address 0xd8 with return code 42::
+
+  $ qemu-system-aarch64 $(QEMU_ARGS) \
+    -plugin ./contrib/plugins/libstoptrigger.so,icount=20:41,addr=0xd4,addr=0xd8:42 -d plugin
+
+The plugin will log the reason of exit, for example::
+
+  0xd4 reached, exiting
+
 Plugin API
 ==========