get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/patches/2218279/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 2218279,
    "url": "http://patchwork.ozlabs.org/api/patches/2218279/?format=api",
    "web_url": "http://patchwork.ozlabs.org/project/qemu-devel/patch/20260331173656.35305-2-xukl2019@sjtu.edu.cn/",
    "project": {
        "id": 14,
        "url": "http://patchwork.ozlabs.org/api/projects/14/?format=api",
        "name": "QEMU Development",
        "link_name": "qemu-devel",
        "list_id": "qemu-devel.nongnu.org",
        "list_email": "qemu-devel@nongnu.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": "",
        "list_archive_url": "",
        "list_archive_url_format": "",
        "commit_url_format": ""
    },
    "msgid": "<20260331173656.35305-2-xukl2019@sjtu.edu.cn>",
    "list_archive_url": null,
    "date": "2026-03-31T17:36:24",
    "name": "[v1,1/3] contrib/plugins: add a zlib compression filter example",
    "commit_ref": null,
    "pull_url": null,
    "state": "new",
    "archived": false,
    "hash": "d6f051894bdcecc3b49b80e680bcb89ce2cdac61",
    "submitter": {
        "id": 93022,
        "url": "http://patchwork.ozlabs.org/api/people/93022/?format=api",
        "name": "XU Kailiang",
        "email": "xukl2019@sjtu.edu.cn"
    },
    "delegate": null,
    "mbox": "http://patchwork.ozlabs.org/project/qemu-devel/patch/20260331173656.35305-2-xukl2019@sjtu.edu.cn/mbox/",
    "series": [
        {
            "id": 498254,
            "url": "http://patchwork.ozlabs.org/api/series/498254/?format=api",
            "web_url": "http://patchwork.ozlabs.org/project/qemu-devel/list/?series=498254",
            "date": "2026-03-31T17:36:25",
            "name": "contrib/plugins: add syscall-filter local-library demos",
            "version": 1,
            "mbox": "http://patchwork.ozlabs.org/series/498254/mbox/"
        }
    ],
    "comments": "http://patchwork.ozlabs.org/api/patches/2218279/comments/",
    "check": "pending",
    "checks": "http://patchwork.ozlabs.org/api/patches/2218279/checks/",
    "tags": {},
    "related": [],
    "headers": {
        "Return-Path": "<qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org>",
        "X-Original-To": "incoming@patchwork.ozlabs.org",
        "Delivered-To": "patchwork-incoming@legolas.ozlabs.org",
        "Authentication-Results": "legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=nongnu.org\n (client-ip=209.51.188.17; helo=lists.gnu.org;\n envelope-from=qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org;\n receiver=patchwork.ozlabs.org)",
        "Received": [
            "from lists.gnu.org (lists.gnu.org [209.51.188.17])\n\t(using TLSv1.2 with cipher ECDHE-ECDSA-AES256-GCM-SHA384 (256/256 bits))\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4flgwL0Vbpz1yFv\n\tfor <incoming@patchwork.ozlabs.org>; Wed, 01 Apr 2026 08:18:58 +1100 (AEDT)",
            "from localhost ([::1] helo=lists1p.gnu.org)\n\tby lists.gnu.org with esmtp (Exim 4.90_1)\n\t(envelope-from <qemu-devel-bounces@nongnu.org>)\n\tid 1w7gTL-0008RN-FX; Tue, 31 Mar 2026 17:18:03 -0400",
            "from eggs.gnu.org ([2001:470:142:3::10])\n by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\n (Exim 4.90_1) (envelope-from <xukl2019@sjtu.edu.cn>)\n id 1w7d8v-0004tN-26\n for qemu-devel@nongnu.org; Tue, 31 Mar 2026 13:44:46 -0400",
            "from smtp232.sjtu.edu.cn ([202.120.2.232])\n by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\n (Exim 4.90_1) (envelope-from <xukl2019@sjtu.edu.cn>)\n id 1w7d8q-00089Y-CB\n for qemu-devel@nongnu.org; Tue, 31 Mar 2026 13:44:44 -0400",
            "from proxy188.sjtu.edu.cn (smtp188.sjtu.edu.cn [202.120.2.188])\n by smtp232.sjtu.edu.cn (Postfix) with ESMTPS id 2BE7A1068BE6E;\n Wed,  1 Apr 2026 01:38:03 +0800 (CST)",
            "from xuklXiaoxin (unknown [202.120.32.222])\n by proxy188.sjtu.edu.cn (Postfix) with ESMTPSA id 04A9A37C8CA;\n Wed,  1 Apr 2026 01:38:03 +0800 (CST)"
        ],
        "From": "XU Kailiang <xukl2019@sjtu.edu.cn>",
        "To": "qemu-devel <qemu-devel@nongnu.org>",
        "Cc": "=?utf-8?q?Alex_Benn=C3=A9e?= <alex.bennee@linaro.org>,\n Pierrick Bouvier <pierrick.bouvier@linaro.org>,\n Alexandre Iooss <erdnaxe@crans.org>, Mahmoud Mandour <ma.mandourr@gmail.com>,\n Ziyang Zhang <functioner@sjtu.edu.cn>, Yun Wang <yunwang94@sjtu.edu.cn>,\n Mingyuan Xia <xiamy@ultrarisc.com>, Zhengwei Qi <qizhwei@sjtu.edu.cn>,\n XU Kailiang <xukl2019@sjtu.edu.cn>",
        "Subject": "[PATCH v1 1/3] contrib/plugins: add a zlib compression filter example",
        "Date": "Wed,  1 Apr 2026 01:36:24 +0800",
        "Message-ID": "<20260331173656.35305-2-xukl2019@sjtu.edu.cn>",
        "X-Mailer": "git-send-email 2.53.0",
        "In-Reply-To": "<20260331173656.35305-1-xukl2019@sjtu.edu.cn>",
        "References": "<20260331173656.35305-1-xukl2019@sjtu.edu.cn>",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "Received-SPF": "pass client-ip=202.120.2.232;\n envelope-from=xukl2019@sjtu.edu.cn;\n helo=smtp232.sjtu.edu.cn",
        "X-Spam_score_int": "1",
        "X-Spam_score": "0.1",
        "X-Spam_bar": "/",
        "X-Spam_report": "(0.1 / 5.0 requ) BAYES_00=-1.9,\n RCVD_IN_VALIDITY_CERTIFIED_BLOCKED=1, RCVD_IN_VALIDITY_RPBL_BLOCKED=1,\n SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=no autolearn_force=no",
        "X-Spam_action": "no action",
        "X-Mailman-Approved-At": "Tue, 31 Mar 2026 17:18:00 -0400",
        "X-BeenThere": "qemu-devel@nongnu.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "qemu development <qemu-devel.nongnu.org>",
        "List-Unsubscribe": "<https://lists.nongnu.org/mailman/options/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=unsubscribe>",
        "List-Archive": "<https://lists.nongnu.org/archive/html/qemu-devel>",
        "List-Post": "<mailto:qemu-devel@nongnu.org>",
        "List-Help": "<mailto:qemu-devel-request@nongnu.org?subject=help>",
        "List-Subscribe": "<https://lists.nongnu.org/mailman/listinfo/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=subscribe>",
        "Errors-To": "qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org",
        "Sender": "qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org"
    },
    "content": "Add a minimal linux-user plugin example that uses the syscall filter\ncallback API to intercept a guest library load and replace it with a\nlocal thunk library.\n\nThe guest-side demo links against libdemo-zlib.so. When the guest\ndynamic loader issues openat() for that library, the plugin returns a\nfile descriptor for libdemo-zlib-thunk.so instead. The thunk library\nforwards compressBound(), compress2(), and uncompress() through magic\nsyscalls, which the plugin handles by calling the host zlib\nimplementation directly.\n\nAt this stage, the loader redirection covers only the openat()-based\nlibrary load path. The follow-up patch in this series extends the same\ndemo to open() and openat2().\n\nOnce the demo library path matches, the plugin asserts that opening the\nthunk library succeeds. That is intentional here: this example is meant\nto show the direct hand-off to a known local thunk library, not fallback\nbehavior when that local library is missing or unusable.\n\nDocument the assumptions and add a small example directory with a\nMakefile so the behavior is easy to reproduce.\n\nSigned-off-by: XU Kailiang <xukl2019@sjtu.edu.cn>\nCo-authored-by: Ziyang Zhang <functioner@sjtu.edu.cn>\n---\n contrib/plugins/meson.build                   |  12 +-\n .../syscall_filter_zlib-example/Makefile      |  19 ++\n .../syscall_filter_zlib-example/README.rst    |  43 +++\n .../zcompress-demo.c                          |  94 ++++++\n .../zcompress-thunk.c                         |  35 +++\n .../zcompress-thunk.h                         |  16 ++\n contrib/plugins/syscall_filter_zlib.c         | 268 ++++++++++++++++++\n docs/about/emulation.rst                      |  41 +++\n 8 files changed, 527 insertions(+), 1 deletion(-)\n create mode 100644 contrib/plugins/syscall_filter_zlib-example/Makefile\n create mode 100644 contrib/plugins/syscall_filter_zlib-example/README.rst\n create mode 100644 contrib/plugins/syscall_filter_zlib-example/zcompress-demo.c\n create mode 100644 contrib/plugins/syscall_filter_zlib-example/zcompress-thunk.c\n create mode 100644 contrib/plugins/syscall_filter_zlib-example/zcompress-thunk.h\n create mode 100644 contrib/plugins/syscall_filter_zlib.c",
    "diff": "diff --git a/contrib/plugins/meson.build b/contrib/plugins/meson.build\nindex 099319e7a1..aa3b95eeab 100644\n--- a/contrib/plugins/meson.build\n+++ b/contrib/plugins/meson.build\n@@ -19,6 +19,12 @@ if host_os != 'windows'\n   contrib_plugins += 'lockstep.c'\n endif\n \n+if host_os != 'windows' and host_os != 'darwin' and\n+   host_machine.endian() == 'little'\n+  # The zlib syscall-filter demo assumes little-endian direct-pointer access.\n+  contrib_plugins += 'syscall_filter_zlib.c'\n+endif\n+\n if 'cpp' in all_languages\n   contrib_plugins += 'cpp.cpp'\n endif\n@@ -26,8 +32,12 @@ endif\n t = []\n if get_option('plugins')\n   foreach i : contrib_plugins\n+    deps = plugins_deps\n+    if i == 'syscall_filter_zlib.c'\n+      deps = [plugins_deps, zlib]\n+    endif\n     t += shared_module(fs.stem(i), files(i),\n-                       dependencies: plugins_deps)\n+                       dependencies: deps)\n   endforeach\n endif\n if t.length() > 0\ndiff --git a/contrib/plugins/syscall_filter_zlib-example/Makefile b/contrib/plugins/syscall_filter_zlib-example/Makefile\nnew file mode 100644\nindex 0000000000..f94b93e950\n--- /dev/null\n+++ b/contrib/plugins/syscall_filter_zlib-example/Makefile\n@@ -0,0 +1,19 @@\n+# SPDX-License-Identifier: GPL-2.0-or-later\n+\n+CC ?= cc\n+CFLAGS ?=\n+LDFLAGS ?=\n+RM ?= rm -f\n+\n+all: libdemo-zlib-thunk.so zcompress-demo\n+\n+libdemo-zlib-thunk.so: zcompress-thunk.c zcompress-thunk.h\n+\t$(CC) $(CFLAGS) -fPIC -shared zcompress-thunk.c -Wl,-soname,libdemo-zlib.so -o $@ $(LDFLAGS)\n+\n+zcompress-demo: zcompress-demo.c zcompress-thunk.h libdemo-zlib-thunk.so\n+\t$(CC) $(CFLAGS) zcompress-demo.c ./libdemo-zlib-thunk.so -Wl,-rpath,'$$ORIGIN' -o $@ $(LDFLAGS)\n+\n+clean:\n+\t$(RM) libdemo-zlib-thunk.so zcompress-demo\n+\n+.PHONY: all clean\ndiff --git a/contrib/plugins/syscall_filter_zlib-example/README.rst b/contrib/plugins/syscall_filter_zlib-example/README.rst\nnew file mode 100644\nindex 0000000000..4920187a2b\n--- /dev/null\n+++ b/contrib/plugins/syscall_filter_zlib-example/README.rst\n@@ -0,0 +1,43 @@\n+.. SPDX-License-Identifier: GPL-2.0-or-later\n+\n+zlib compression syscall-filter example\n+=======================================\n+\n+This directory contains the guest-side pieces used by\n+``contrib/plugins/syscall_filter_zlib.c``:\n+\n+* ``zcompress-demo.c`` is linked against ``libdemo-zlib.so`` and calls the\n+  compression helpers directly.\n+* The plugin intercepts the loader's ``openat()`` call and returns a file\n+  descriptor for ``./libdemo-zlib-thunk.so`` instead.\n+* ``zcompress-thunk.c`` exposes a tiny compression API as thin wrappers around\n+  magic syscalls.\n+* The plugin filters those magic syscalls and executes the host zlib\n+  ``compressBound()``, ``compress2()``, and ``uncompress()`` implementations\n+  directly on guest buffers.\n+* The example currently supports ``x86_64`` linux-user only. Extending the\n+  syscall-number table for more targets is straightforward, but is outside the\n+  scope of this patch.\n+* To keep the demo small, the plugin assumes ``guest_base == 0`` on a\n+  little-endian 64-bit host. For this x86_64 linux-user demo, that means guest\n+  virtual addresses are directly usable as host pointers. In practice that\n+  means running ``qemu-x86_64`` on a little-endian 64-bit host without forcing\n+  a nonzero guest base.\n+\n+Build the guest-side demo with::\n+\n+  make\n+\n+Then run it from this directory with QEMU linux-user and the plugin::\n+\n+  QEMU_BUILD=/path/to/qemu/build\n+  $QEMU_BUILD/qemu-x86_64 \\\n+    -plugin $QEMU_BUILD/contrib/plugins/libsyscall_filter_zlib.so \\\n+    -d plugin \\\n+    ./zcompress-demo\n+\n+The build links ``zcompress-demo`` against ``libdemo-zlib-thunk.so`` while\n+giving that shared object the soname ``libdemo-zlib.so``. Without the plugin,\n+the program fails at startup because no ``libdemo-zlib.so`` file exists in the\n+runtime search path. With the plugin, the guest sees a working library load\n+even though the loader actually receives ``libdemo-zlib-thunk.so``.\ndiff --git a/contrib/plugins/syscall_filter_zlib-example/zcompress-demo.c b/contrib/plugins/syscall_filter_zlib-example/zcompress-demo.c\nnew file mode 100644\nindex 0000000000..a199b8d290\n--- /dev/null\n+++ b/contrib/plugins/syscall_filter_zlib-example/zcompress-demo.c\n@@ -0,0 +1,94 @@\n+/*\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include <stdint.h>\n+#include <stdio.h>\n+#include <stdlib.h>\n+#include <string.h>\n+\n+#include \"zcompress-thunk.h\"\n+\n+#define INPUT_SIZE (8 * 1024 * 1024)\n+#define Z_BEST_COMPRESSION 9\n+\n+static void fill_input(unsigned char *data, size_t len)\n+{\n+    static const unsigned char pattern[] =\n+        \"QEMU syscall filter zlib compression demo payload\\n\";\n+    size_t i;\n+\n+    for (i = 0; i < len; i++) {\n+        data[i] = pattern[i % (sizeof(pattern) - 1)];\n+        if ((i % 4096) == 0) {\n+            data[i] = (unsigned char)(i >> 12);\n+        }\n+    }\n+}\n+\n+int main(void)\n+{\n+    unsigned char *input = NULL;\n+    unsigned char *compressed = NULL;\n+    unsigned char *output = NULL;\n+    size_t compressed_len;\n+    size_t output_len;\n+    size_t compressed_bound;\n+    int ret = EXIT_FAILURE;\n+\n+    input = malloc(INPUT_SIZE);\n+    if (input == NULL) {\n+        perror(\"malloc\");\n+        goto cleanup;\n+    }\n+\n+    fill_input(input, INPUT_SIZE);\n+\n+    compressed_bound = zcompress_compress_bound(INPUT_SIZE);\n+    if (compressed_bound == 0) {\n+        fprintf(stderr, \"zcompress_compress_bound failed\\n\");\n+        goto cleanup;\n+    }\n+\n+    compressed = malloc(compressed_bound);\n+    output = malloc(INPUT_SIZE);\n+    if (compressed == NULL || output == NULL) {\n+        perror(\"malloc\");\n+        goto cleanup;\n+    }\n+\n+    compressed_len = compressed_bound;\n+    if (zcompress_compress(input, INPUT_SIZE, compressed, &compressed_len,\n+                           Z_BEST_COMPRESSION) != 0) {\n+        fprintf(stderr, \"zcompress_compress failed\\n\");\n+        goto cleanup;\n+    }\n+\n+    output_len = INPUT_SIZE;\n+    if (zcompress_uncompress(compressed, compressed_len, output,\n+                             &output_len) != 0) {\n+        fprintf(stderr, \"zcompress_uncompress failed\\n\");\n+        goto cleanup;\n+    }\n+\n+    if (output_len != INPUT_SIZE || memcmp(input, output, INPUT_SIZE) != 0) {\n+        fprintf(stderr, \"round-trip mismatch\\n\");\n+        goto cleanup;\n+    }\n+\n+    if (compressed_len >= INPUT_SIZE) {\n+        fprintf(stderr, \"compressed output was not smaller than input\\n\");\n+        goto cleanup;\n+    }\n+\n+    printf(\"zlib demo compressed %u bytes to %zu bytes\\n\",\n+           INPUT_SIZE, compressed_len);\n+    puts(\"zlib demo round-tripped successfully\");\n+    ret = EXIT_SUCCESS;\n+\n+cleanup:\n+    free(output);\n+    free(compressed);\n+    free(input);\n+    return ret;\n+}\ndiff --git a/contrib/plugins/syscall_filter_zlib-example/zcompress-thunk.c b/contrib/plugins/syscall_filter_zlib-example/zcompress-thunk.c\nnew file mode 100644\nindex 0000000000..1e498a728e\n--- /dev/null\n+++ b/contrib/plugins/syscall_filter_zlib-example/zcompress-thunk.c\n@@ -0,0 +1,35 @@\n+/*\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include <stdint.h>\n+#include <unistd.h>\n+\n+#include \"zcompress-thunk.h\"\n+\n+#define ZLIB_COMPRESS_MAGIC_SYSCALL 4096\n+#define ZLIB_COMPRESS_OP_BOUND 1\n+#define ZLIB_COMPRESS_OP_COMPRESS2 2\n+#define ZLIB_COMPRESS_OP_UNCOMPRESS 3\n+\n+size_t zcompress_compress_bound(size_t source_len)\n+{\n+    return (size_t)syscall(ZLIB_COMPRESS_MAGIC_SYSCALL,\n+                           ZLIB_COMPRESS_OP_BOUND, source_len);\n+}\n+\n+int zcompress_compress(const void *source, size_t source_len,\n+                       void *dest, size_t *dest_len, int level)\n+{\n+    return (int)syscall(ZLIB_COMPRESS_MAGIC_SYSCALL,\n+                        ZLIB_COMPRESS_OP_COMPRESS2,\n+                        source, source_len, dest, dest_len, level);\n+}\n+\n+int zcompress_uncompress(const void *source, size_t source_len,\n+                         void *dest, size_t *dest_len)\n+{\n+    return (int)syscall(ZLIB_COMPRESS_MAGIC_SYSCALL,\n+                        ZLIB_COMPRESS_OP_UNCOMPRESS,\n+                        source, source_len, dest, dest_len);\n+}\ndiff --git a/contrib/plugins/syscall_filter_zlib-example/zcompress-thunk.h b/contrib/plugins/syscall_filter_zlib-example/zcompress-thunk.h\nnew file mode 100644\nindex 0000000000..f0092ff681\n--- /dev/null\n+++ b/contrib/plugins/syscall_filter_zlib-example/zcompress-thunk.h\n@@ -0,0 +1,16 @@\n+/*\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#ifndef CONTRIB_PLUGINS_SYSCALL_FILTER_ZLIB_EXAMPLE_ZCOMPRESS_THUNK_H\n+#define CONTRIB_PLUGINS_SYSCALL_FILTER_ZLIB_EXAMPLE_ZCOMPRESS_THUNK_H\n+\n+#include <stddef.h>\n+\n+size_t zcompress_compress_bound(size_t source_len);\n+int zcompress_compress(const void *source, size_t source_len,\n+                       void *dest, size_t *dest_len, int level);\n+int zcompress_uncompress(const void *source, size_t source_len,\n+                         void *dest, size_t *dest_len);\n+\n+#endif\ndiff --git a/contrib/plugins/syscall_filter_zlib.c b/contrib/plugins/syscall_filter_zlib.c\nnew file mode 100644\nindex 0000000000..e8f430cbaf\n--- /dev/null\n+++ b/contrib/plugins/syscall_filter_zlib.c\n@@ -0,0 +1,268 @@\n+/*\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ *\n+ * Minimal linux-user plugin that demonstrates syscall filtering for\n+ * local library interception with a host zlib compression example.\n+ *\n+ * When the guest dynamic loader attempts to open \"./libdemo-zlib.so\", this\n+ * plugin intercepts openat() and instead returns a file descriptor for\n+ * \"libdemo-zlib-thunk.so\" in the same directory. The thunk library then\n+ * forwards compression requests through magic syscalls, which are handled by\n+ * this plugin and executed by the host's zlib implementation.\n+ *\n+ * This demo intentionally assumes a linux-user run with guest_base == 0 on a\n+ * little-endian 64-bit host, so guest virtual addresses are directly usable\n+ * as host pointers.\n+ */\n+\n+#include <fcntl.h>\n+#include <glib.h>\n+#include <inttypes.h>\n+#include <stdbool.h>\n+#include <stddef.h>\n+#include <stdint.h>\n+#include <stdio.h>\n+#include <string.h>\n+#include <unistd.h>\n+#include <zlib.h>\n+\n+#include <qemu-plugin.h>\n+\n+QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION;\n+\n+#define MAGIC_SYSCALL 4096\n+#define ZLIB_COMPRESS_OP_BOUND 1\n+#define ZLIB_COMPRESS_OP_COMPRESS2 2\n+#define ZLIB_COMPRESS_OP_UNCOMPRESS 3\n+#define ZLIB_COMPRESS_LIBRARY \"libdemo-zlib.so\"\n+#define ZLIB_COMPRESS_THUNK_LIBRARY \"libdemo-zlib-thunk.so\"\n+#define ZLIB_COMPRESS_MAX_INPUT (64 * 1024 * 1024)\n+#define ZLIB_COMPRESS_MAX_BUFFER (128 * 1024 * 1024)\n+#define GUEST_STRING_CHUNK 64\n+#define GUEST_STRING_LIMIT (1 << 20)\n+#define X86_64_OPENAT_NR 257\n+\n+static char *read_guest_cstring(uint64_t addr)\n+{\n+    g_autoptr(GByteArray) data = g_byte_array_sized_new(GUEST_STRING_CHUNK);\n+    g_autoptr(GString) str = g_string_sized_new(GUEST_STRING_CHUNK);\n+    size_t offset;\n+\n+    for (offset = 0;\n+         offset < GUEST_STRING_LIMIT;\n+         offset += GUEST_STRING_CHUNK) {\n+        g_byte_array_set_size(data, GUEST_STRING_CHUNK);\n+        if (!qemu_plugin_read_memory_vaddr(addr + offset, data,\n+                                           GUEST_STRING_CHUNK)) {\n+            return NULL;\n+        }\n+\n+        for (guint i = 0; i < data->len; i++) {\n+            if (data->data[i] == '\\0') {\n+                return g_string_free(g_steal_pointer(&str), FALSE);\n+            }\n+            g_string_append_c(str, data->data[i]);\n+        }\n+    }\n+\n+    return NULL;\n+}\n+\n+static bool guest_path_matches_zlib_compress(const char *path)\n+{\n+    g_autofree char *basename = g_path_get_basename(path);\n+\n+    return strcmp(basename, ZLIB_COMPRESS_LIBRARY) == 0;\n+}\n+\n+static char *build_thunk_path(const char *path)\n+{\n+    g_autofree char *dirname = g_path_get_dirname(path);\n+\n+    if (strcmp(dirname, \".\") == 0) {\n+        return g_strdup(ZLIB_COMPRESS_THUNK_LIBRARY);\n+    }\n+\n+    return g_build_filename(dirname, ZLIB_COMPRESS_THUNK_LIBRARY, NULL);\n+}\n+\n+static bool handle_library_open(int64_t num, uint64_t a1, uint64_t a2,\n+                                uint64_t a3, uint64_t a4, uint64_t *sysret)\n+{\n+    g_autofree char *path = NULL;\n+    g_autofree char *thunk_path = NULL;\n+    g_autofree char *out = NULL;\n+    int fd;\n+\n+    if (num != X86_64_OPENAT_NR) {\n+        return false;\n+    }\n+\n+    path = read_guest_cstring(a2);\n+    if (path == NULL || !guest_path_matches_zlib_compress(path)) {\n+        return false;\n+    }\n+    thunk_path = build_thunk_path(path);\n+    if (access(thunk_path, F_OK) != 0) {\n+        return false;\n+    }\n+\n+    fd = openat((int)a1, thunk_path, (int)a3, (mode_t)a4);\n+    g_assert(fd >= 0);\n+\n+    *sysret = fd;\n+    out = g_strdup_printf(\"syscall_filter_zlib: redirected %s -> %s (fd=%d)\\n\",\n+                          path, thunk_path, fd);\n+    qemu_plugin_outs(out);\n+    return true;\n+}\n+\n+static bool handle_compress_bound(int64_t num, uint64_t a1, uint64_t a2,\n+                                  uint64_t *sysret)\n+{\n+    if (num != MAGIC_SYSCALL || a1 != ZLIB_COMPRESS_OP_BOUND) {\n+        return false;\n+    }\n+\n+    if (a2 > ZLIB_COMPRESS_MAX_INPUT) {\n+        *sysret = 0;\n+        return true;\n+    }\n+\n+    *sysret = compressBound((uLong)a2);\n+    return true;\n+}\n+\n+static bool handle_compress2(int64_t num, uint64_t a1, uint64_t a2,\n+                             uint64_t a3, uint64_t a4, uint64_t a5,\n+                             uint64_t a6, uint64_t *sysret)\n+{\n+    g_autofree char *out = NULL;\n+    const Bytef *source;\n+    Bytef *dest;\n+    uLongf *dest_lenp;\n+    uLongf guest_dest_len;\n+    int status;\n+    int level = (int)a6;\n+\n+    if (num != MAGIC_SYSCALL || a1 != ZLIB_COMPRESS_OP_COMPRESS2) {\n+        return false;\n+    }\n+\n+    g_assert(a2 != 0 && a4 != 0 && a5 != 0);\n+    g_assert(level == Z_DEFAULT_COMPRESSION ||\n+             (level >= Z_NO_COMPRESSION && level <= Z_BEST_COMPRESSION));\n+\n+    dest_lenp = (uLongf *)(uintptr_t)a5;\n+    guest_dest_len = *dest_lenp;\n+\n+    g_assert(a3 <= ZLIB_COMPRESS_MAX_INPUT);\n+    g_assert(guest_dest_len <= ZLIB_COMPRESS_MAX_BUFFER);\n+\n+    source = (const Bytef *)(uintptr_t)a2;\n+    dest = (Bytef *)(uintptr_t)a4;\n+    *dest_lenp = guest_dest_len;\n+    status = compress2(dest, dest_lenp, source, (uLong)a3, level);\n+\n+    if (status != Z_OK) {\n+        *sysret = (uint64_t)(uint32_t)status;\n+        return true;\n+    }\n+\n+    *sysret = Z_OK;\n+    out = g_strdup_printf(\n+        \"syscall_filter_zlib: compressed %\" PRIu64\n+        \" guest bytes to %lu host bytes\\n\",\n+                          a3, (unsigned long)*dest_lenp);\n+    qemu_plugin_outs(out);\n+    return true;\n+}\n+\n+static bool handle_uncompress(int64_t num, uint64_t a1, uint64_t a2,\n+                              uint64_t a3, uint64_t a4, uint64_t a5,\n+                              uint64_t *sysret)\n+{\n+    g_autofree char *out = NULL;\n+    const Bytef *source;\n+    Bytef *dest;\n+    uLongf *dest_lenp;\n+    uLongf guest_dest_len;\n+    int status;\n+\n+    if (num != MAGIC_SYSCALL || a1 != ZLIB_COMPRESS_OP_UNCOMPRESS) {\n+        return false;\n+    }\n+\n+    g_assert(a2 != 0 && a4 != 0 && a5 != 0);\n+\n+    dest_lenp = (uLongf *)(uintptr_t)a5;\n+    guest_dest_len = *dest_lenp;\n+\n+    g_assert(a3 <= ZLIB_COMPRESS_MAX_BUFFER);\n+    g_assert(guest_dest_len <= ZLIB_COMPRESS_MAX_INPUT);\n+\n+    source = (const Bytef *)(uintptr_t)a2;\n+    dest = (Bytef *)(uintptr_t)a4;\n+    *dest_lenp = guest_dest_len;\n+    status = uncompress(dest, dest_lenp, source, (uLong)a3);\n+\n+    if (status != Z_OK) {\n+        *sysret = (uint64_t)(uint32_t)status;\n+        return true;\n+    }\n+\n+    *sysret = Z_OK;\n+    out = g_strdup_printf(\n+        \"syscall_filter_zlib: uncompressed %\" PRIu64\n+        \" guest bytes to %lu host bytes\\n\",\n+                          a3, (unsigned long)*dest_lenp);\n+    qemu_plugin_outs(out);\n+    return true;\n+}\n+\n+static bool vcpu_syscall_filter(qemu_plugin_id_t id, unsigned int vcpu_index,\n+                                int64_t num, uint64_t a1, uint64_t a2,\n+                                uint64_t a3, uint64_t a4, uint64_t a5,\n+                                uint64_t a6, uint64_t a7, uint64_t a8,\n+                                uint64_t *sysret)\n+{\n+    if (handle_library_open(num, a1, a2, a3, a4, sysret)) {\n+        return true;\n+    }\n+\n+    if (handle_compress_bound(num, a1, a2, sysret)) {\n+        return true;\n+    }\n+\n+    if (handle_compress2(num, a1, a2, a3, a4, a5, a6, sysret)) {\n+        return true;\n+    }\n+\n+    if (handle_uncompress(num, a1, a2, a3, a4, a5, sysret)) {\n+        return true;\n+    }\n+\n+    return false;\n+}\n+\n+QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,\n+                                           const qemu_info_t *info,\n+                                           int argc, char **argv)\n+{\n+    if (argc != 0) {\n+        fprintf(stderr,\n+                \"syscall_filter_zlib: this example plugin does not take arguments\\n\");\n+        return -1;\n+    }\n+\n+    if (strcmp(info->target_name, \"x86_64\") != 0) {\n+        fprintf(stderr,\n+                \"syscall_filter_zlib: unsupported linux-user target '%s' \"\n+                \"(supported: x86_64)\\n\",\n+                info->target_name);\n+        return -1;\n+    }\n+\n+    qemu_plugin_register_vcpu_syscall_filter_cb(id, vcpu_syscall_filter);\n+    return 0;\n+}\ndiff --git a/docs/about/emulation.rst b/docs/about/emulation.rst\nindex 469f31bab6..4afec85ff6 100644\n--- a/docs/about/emulation.rst\n+++ b/docs/about/emulation.rst\n@@ -235,6 +235,47 @@ basic plugins that are used to test and exercise the API during the\n ``make check-tcg`` target in ``tests/tcg/plugins`` that are never the\n less useful for basic analysis.\n \n+Local Library Interception\n+..........................\n+\n+``contrib/plugins/syscall_filter_zlib.c``\n+\n+This linux-user example shows how to use the syscall filter callback API to\n+intercept a library load and replace it with a guest thunk library. The guest\n+side example lives in ``contrib/plugins/syscall_filter_zlib-example``.\n+\n+The plugin does two things:\n+\n+* It filters the guest ``openat()`` that the dynamic loader issues for\n+  ``./libdemo-zlib.so`` and instead returns a file descriptor for\n+  ``libdemo-zlib-thunk.so``.\n+* It filters magic syscalls from the thunk library and runs the host's zlib\n+  ``compressBound()``, ``compress2()``, and ``uncompress()`` implementations\n+  directly on guest buffers.\n+\n+This makes the motivation concrete without introducing callback semantics: the\n+example looks like a plausible local compression offload, and the guest-visible\n+API stays small and easy to reproduce.\n+The example currently supports ``x86_64`` linux-user only. To keep the example\n+minimal, it currently assumes ``guest_base == 0`` on a little-endian 64-bit\n+host. For this x86_64 linux-user demo, that means guest virtual addresses are\n+directly usable as host pointers.\n+\n+To reproduce the example on ``qemu-x86_64``::\n+\n+  $ ninja -C build contrib-plugins\n+  $ cd contrib/plugins/syscall_filter_zlib-example\n+  $ make\n+  $ QEMU_BUILD=/path/to/qemu/build\n+  $ $QEMU_BUILD/qemu-x86_64 \\\n+      -plugin $QEMU_BUILD/contrib/plugins/libsyscall_filter_zlib.so \\\n+      -d plugin \\\n+      ./zcompress-demo\n+\n+Without the plugin, the guest program fails at startup because\n+``libdemo-zlib.so`` is missing. With the plugin, QEMU's plugin log shows the\n+loader redirection plus native compression and decompression handling.\n+\n Empty\n .....\n \n",
    "prefixes": [
        "v1",
        "1/3"
    ]
}