Refactor plugin registration and add ABI/license check
authorJoão Valverde <j@v6e.pt>
Fri, 1 Dec 2023 13:52:27 +0000 (13:52 +0000)
committerJoão Valverde <j@v6e.pt>
Sun, 3 Dec 2023 23:20:39 +0000 (23:20 +0000)
Remove the boilerplate and multiple symbols required
to write and load a plugin and use a simpler
interface that is also much more featureful and
extensible.

This removes the major.minor ABI check in favor of an
ABI version number that is simpler and more flexible.

In practice our 3 separate ABI versions still increase
with each major.minor version number but this may change
in the future, particularly for codecs that have a much
narrower and stable codec API.

There is a new license compatibility check. Plugins
must declare if the use GPLv2 or later, or alternatively
an FSF compliant GPLv2 compatible license. Otherwise
Wireshark refuses to load the plugin. The SPDX ID is
just an ancillary field and not enforced currently.

Adds more metadata fields during plugin registration.
These are only used for display and debugging.

There is an extra API level number in the registration API
that is reserved for future use.

CMakeLists.txt
doc/plugins.example/hello.c
plugins/epan/dfilter/ipaddr/ipaddr.c
tools/make-plugin-reg.py
ws_version.h.in
wsutil/plugins.c
wsutil/plugins.h

index 0044b7588882015708b0ac6e388a254bc0a75b9d..e10df0e04abb033091eb24acb74f00b9eaa8775b 100644 (file)
@@ -59,6 +59,22 @@ set(PROJECT_MAJOR_VERSION 4)
 set(PROJECT_MINOR_VERSION 3)
 set(PROJECT_PATCH_VERSION 0)
 set(PROJECT_BUILD_VERSION 0)
+# ABI version for plugins. It must be incremented by one for
+# every odd minor release number.
+# start at 1 (arbitrary).
+# release 4.3.X (unstable) = ABI version 1 (start)
+# release 4.4.X (stable)   = ABI version 1 (stable release, freeze ABI 1)
+# release 4.5.X (unstable) = ABI version 2 (increment for new unstable release, ABI break)
+# release 4.6.X (stable)   = ABI version 2 (stable release, freeze ABI 2)
+# release 4.7.X (unstable) = ABI version 3 (increment for new unstable release, ABI break)
+# release 5.0.X (stable)   = ABI version 3 (stable release, freeze ABI 3)
+# etc.
+set(PROJECT_ABI_VERSION_EPAN    1)
+set(PROJECT_ABI_VERSION_WIRETAP 1)
+# Codecs API/ABI is much more narrow and stable than the other two so this
+# may not need to be incremented every X.Y release.
+set(PROJECT_ABI_VERSION_CODEC   1)
+
 set(PROJECT_VERSION_EXTENSION "")
 
 if(DEFINED ENV{WIRESHARK_VERSION_EXTRA})
index 5a9ccd81696b9cf0ffbbf593c9f939f7d057ed47..19178bfeab29ef92c33fe92207e1d3302d2ee55c 100644 (file)
 #define VERSION "0.0.0"
 #endif
 
-WS_DLL_PUBLIC_DEF const char plugin_version[] = VERSION;
-WS_DLL_PUBLIC_DEF const int plugin_want_major = WIRESHARK_VERSION_MAJOR;
-WS_DLL_PUBLIC_DEF const int plugin_want_minor = WIRESHARK_VERSION_MINOR;
-
-WS_DLL_PUBLIC void plugin_register(void);
-WS_DLL_PUBLIC uint32_t plugin_describe(void);
-
 static int proto_hello;
 static dissector_handle_t handle_hello;
 
 static int
 dissect_hello(tvbuff_t *tvb, packet_info *pinfo _U_, proto_tree *tree, void *data _U_)
 {
-    proto_tree_add_protocol_format(tree, proto_hello, tvb, 0, -1, "This is Hello version %s, a Wireshark postdissector plugin prototype", plugin_version);
+    proto_tree_add_protocol_format(tree, proto_hello, tvb, 0, -1, "This is Hello version %s, a Wireshark postdissector plugin prototype", VERSION);
     return tvb_captured_length(tvb);
 }
 
@@ -48,7 +41,7 @@ proto_reg_handoff_hello(void)
     /* empty */
 }
 
-void
+static void
 plugin_register(void)
 {
     static proto_plugin plug;
@@ -58,8 +51,14 @@ plugin_register(void)
     proto_register_plugin(&plug);
 }
 
-uint32_t
-plugin_describe(void)
-{
-    return WS_PLUGIN_DESC_DISSECTOR;
-}
+static struct ws_module module = {
+    .license = WS_PLUGIN_IS_GPLv2_OR_LATER,
+    .flags = WS_PLUGIN_DESC_DISSECTOR,
+    .version = VERSION,
+    .spdx_id = "GPL-2.0-or-later",
+    .home_url = "Your-URL-here",
+    .blurb = "Hello world for Wireshark plugin development",
+    .register_cb = &plugin_register,
+};
+
+WIRESHARK_PLUGIN_REGISTER_EPAN(&module, 0)
index f6f7ded5e74623fa9688d5d5aa00e0f304265dbb..857eb1f3f7936a4d1ae9fd9e3a989eb54ab07b07 100644 (file)
 #define PLUGIN_VERSION "0.0.0"
 #endif
 
-WS_DLL_PUBLIC_DEF const gchar plugin_version[] = PLUGIN_VERSION;
-WS_DLL_PUBLIC_DEF const int plugin_want_major = WIRESHARK_VERSION_MAJOR;
-WS_DLL_PUBLIC_DEF const int plugin_want_minor = WIRESHARK_VERSION_MINOR;
-
-WS_DLL_PUBLIC void plugin_register(void);
-WS_DLL_PUBLIC uint32_t plugin_describe(void);
-
 typedef bool (*ip_is_func_t)(fvalue_t *);
 
 static const struct ws_iana_ip_special_block *
@@ -345,7 +338,7 @@ cleanup(void)
     df_func_deregister(&func_ip_is_ula);
 }
 
-void
+static void
 plugin_register(void)
 {
     static dfilter_plugin plug;
@@ -355,8 +348,14 @@ plugin_register(void)
     dfilter_plugins_register(&plug);
 }
 
-uint32_t
-plugin_describe(void)
-{
-    return WS_PLUGIN_DESC_DFILTER;
-}
+static struct ws_module module = {
+    .license = WS_PLUGIN_IS_GPLv2_OR_LATER,
+    .flags = WS_PLUGIN_DESC_DFILTER,
+    .version = PLUGIN_VERSION,
+    .spdx_id = WS_PLUGIN_SPDX_GPLv2,
+    .home_url = WS_PLUGIN_GITLAB_URL,
+    .blurb = "Display filter functions to test IP addresses",
+    .register_cb = &plugin_register,
+};
+
+WIRESHARK_PLUGIN_REGISTER_EPAN(&module, 0)
index cd2de2f21d0018d8a0479058fc3595aa76ecc753..34df09a8477e9df92a35ad8cd3e23e237f22c120 100755 (executable)
@@ -19,6 +19,11 @@ srcdir = sys.argv[1]
 #
 registertype = sys.argv[2]
 #
+# The third argument is the plugin short description
+#
+#plugin_blurb = sys.argv[3]
+plugin_blurb = "FIXME"
+#
 # All subsequent arguments are the files to scan.
 #
 files = sys.argv[3:]
@@ -140,29 +145,11 @@ for symbol in regs['codec_register']:
 for symbol in regs['register_tap_listener']:
     reg_code += "void register_tap_listener_%s(void);\n" % (symbol)
 
-DESCRIPTION_FLAG = {
-    'plugin': 'WS_PLUGIN_DESC_DISSECTOR',
-    'plugin_wtap': 'WS_PLUGIN_DESC_FILE_TYPE',
-    'plugin_codec': 'WS_PLUGIN_DESC_CODEC',
-    'plugin_tap': 'WS_PLUGIN_DESC_TAP_LISTENER'
-}
-
 reg_code += """
-WS_DLL_PUBLIC_DEF const gchar plugin_version[] = PLUGIN_VERSION;
-WS_DLL_PUBLIC_DEF const int plugin_want_major = VERSION_MAJOR;
-WS_DLL_PUBLIC_DEF const int plugin_want_minor = VERSION_MINOR;
-
-WS_DLL_PUBLIC void plugin_register(void);
-WS_DLL_PUBLIC uint32_t plugin_describe(void);
-
-uint32_t plugin_describe(void)
-{
-    return %s;
-}
-
-void plugin_register(void)
+static void
+plugin_register(void)
 {
-""" % DESCRIPTION_FLAG[registertype]
+"""
 
 if registertype == "plugin":
     for symbol in regs['proto_reg']:
@@ -191,6 +178,34 @@ if registertype == "plugin_tap":
 
 reg_code += "}\n"
 
+DESCRIPTION_FLAG = {
+    'plugin': 'WS_PLUGIN_DESC_DISSECTOR',
+    'plugin_wtap': 'WS_PLUGIN_DESC_FILE_TYPE',
+    'plugin_codec': 'WS_PLUGIN_DESC_CODEC',
+    'plugin_tap': 'WS_PLUGIN_DESC_TAP_LISTENER'
+}
+
+PLUGIN_REGISTER = {
+    'plugin': 'WIRESHARK_PLUGIN_REGISTER_EPAN',
+    'plugin_wtap': 'WIRESHARK_PLUGIN_REGISTER_WIRETAP',
+    'plugin_codec': 'WIRESHARK_PLUGIN_REGISTER_CODEC',
+    'plugin_tap': 'WIRESHARK_PLUGIN_REGISTER_EPAN'
+}
+
+reg_code += """
+static struct ws_module module = {
+    .license = WS_PLUGIN_IS_GPLv2_OR_LATER,
+    .flags = %s,
+    .version = PLUGIN_VERSION,
+    .spdx_id = WS_PLUGIN_SPDX_GPLv2,
+    .home_url = WS_PLUGIN_GITLAB_URL,
+    .blurb = "%s",
+    .register_cb = &plugin_register,
+};
+
+%s(&module, 0)
+""" % (DESCRIPTION_FLAG[registertype], plugin_blurb, PLUGIN_REGISTER[registertype])
+
 try:
     fh = open(final_filename, 'w')
     fh.write(reg_code)
index f89bfb80e20bf2e7fc82fe0ff1cc7edae016d79a..3e6548d83c10fe1447ac4ca3a8eb8662d3809acd 100644 (file)
@@ -7,4 +7,9 @@
 #define WIRESHARK_VERSION_MINOR @PROJECT_MINOR_VERSION@
 #define WIRESHARK_VERSION_MICRO @PROJECT_PATCH_VERSION@
 
+/* ABI version for plugin compatibility. */
+#define WIRESHARK_ABI_VERSION_EPAN     @PROJECT_ABI_VERSION_EPAN@
+#define WIRESHARK_ABI_VERSION_WIRETAP  @PROJECT_ABI_VERSION_WIRETAP@
+#define WIRESHARK_ABI_VERSION_CODEC    @PROJECT_ABI_VERSION_CODEC@
+
 #endif /* __WS_VERSION_H__ */
index cbb523ee4a686be3319373f0e7f2ab26d8083d49..0a977c7f3a95acad9af64a7eabb5100d9872041d 100644 (file)
@@ -29,8 +29,7 @@
 typedef struct _plugin {
     GModule        *handle;       /* handle returned by g_module_open */
     char           *name;         /* plugin name */
-    const char     *version;      /* plugin version */
-    uint32_t        flags;        /* plugin flags */
+    struct ws_module *module;
 } plugin;
 
 #define TYPE_DIR_EPAN       "epan"
@@ -57,6 +56,22 @@ type_to_dir(plugin_type_e type)
     ws_assert_not_reached();
 }
 
+static inline const char *
+type_to_name(plugin_type_e type)
+{
+    switch (type) {
+    case WS_PLUGIN_EPAN:
+        return "epan";
+    case WS_PLUGIN_WIRETAP:
+        return "wiretap";
+    case WS_PLUGIN_CODEC:
+        return "codec";
+    default:
+        return "unknown";
+    }
+    ws_assert_not_reached();
+}
+
 static inline const char *
 flags_to_str(uint32_t flags)
 {
@@ -94,26 +109,20 @@ compare_plugins(gconstpointer a, gconstpointer b)
 }
 
 static bool
-pass_plugin_version_compatibility(GModule *handle, const char *name)
+pass_plugin_compatibility(const char *name, plugin_type_e type,
+                            int abi_version, int min_api_level _U_,
+                            struct ws_module *module)
 {
-    void * symb;
-    int major, minor;
-
-    if(!g_module_symbol(handle, "plugin_want_major", &symb)) {
-        report_failure("The plugin '%s' has no \"plugin_want_major\" symbol", name);
+    if (abi_version != plugins_abi_version(type)) {
+        report_failure("The plugin '%s' has incompatible ABI, have version %d, expected %d",
+                            name, abi_version, plugins_abi_version(type));
         return false;
     }
-    major = *(int *)symb;
 
-    if(!g_module_symbol(handle, "plugin_want_minor", &symb)) {
-        report_failure("The plugin '%s' has no \"plugin_want_minor\" symbol", name);
-        return false;
-    }
-    minor = *(int *)symb;
-
-    if (major != VERSION_MAJOR || minor != VERSION_MINOR) {
-        report_failure("The plugin '%s' was compiled for Wireshark version %d.%d",
-                            name, major, minor);
+    if (module->license != WS_PLUGIN_IS_GPLv2_OR_LATER &&
+                module->license != WS_PLUGIN_IS_GPLv2_COMPATIBLE) {
+        report_failure("The plugin '%s' is not GPLv2 compatible (invalid license 0x%x, with SPDX ID \"%s\")",
+                                name, module->license, module->spdx_id);
         return false;
     }
 
@@ -135,10 +144,12 @@ scan_plugins_dir(GHashTable *plugins_module, const char *dirpath, plugin_type_e
     char          *plugin_folder;
     char          *plugin_file;     /* current file full path */
     GModule       *handle;          /* handle returned by g_module_open */
-    void *         symbol;
-    const char    *plug_version;
-    uint32_t       flags;
+    void          *symbol;
     plugin        *new_plug;
+    plugin_type_e have_type;
+    int            abi_version;
+    int            min_api_level;
+    struct ws_module *module;
 
     if (append_type)
         plugin_folder = g_build_filename(dirpath, type_to_dir(type), (char *)NULL);
@@ -178,45 +189,41 @@ scan_plugins_dir(GHashTable *plugins_module, const char *dirpath, plugin_type_e
             continue;
         }
 
-        if (!g_module_symbol(handle, "plugin_version", &symbol))
-        {
-            report_failure("The plugin '%s' has no \"plugin_version\" symbol", name);
+        /* Search for the entry point for the plugin registration function */
+        if (!g_module_symbol(handle, "wireshark_load_module", &symbol)) {
+            report_failure("The plugin '%s' has no \"wireshark_load_module\" symbol", name);
             g_module_close(handle);
             g_free(plugin_file);
             continue;
         }
-        plug_version = (const char *)symbol;
 
-        if (!pass_plugin_version_compatibility(handle, name)) {
+DIAG_OFF_PEDANTIC
+        /* Found it, load module. */
+        have_type = ((ws_load_module_func)symbol)(&abi_version, &min_api_level, &module);
+DIAG_ON_PEDANTIC
+
+        if (have_type != type) {
+            // Should not happen. Our filesystem hierarchy uses plugin type.
+            report_failure("The plugin '%s' has invalid type, expected %s, have %s",
+                                name, type_to_name(type), type_to_name(have_type));
             g_module_close(handle);
             g_free(plugin_file);
             continue;
         }
 
-        /* Search for the entry point for the plugin registration function */
-        if (!g_module_symbol(handle, "plugin_register", &symbol)) {
-            report_failure("The plugin '%s' has no \"plugin_register\" symbol", name);
+        if (!pass_plugin_compatibility(name, type, abi_version, min_api_level, module)) {
             g_module_close(handle);
             g_free(plugin_file);
             continue;
         }
 
-DIAG_OFF_PEDANTIC
-        /* Found it, call the plugin registration function. */
-        ((plugin_register_func)symbol)();
-DIAG_ON_PEDANTIC
-
-        /* Search for the (optional) description flag registration function */
-        if (g_module_symbol(handle, "plugin_describe", &symbol))
-            flags = ((plugin_describe_func)symbol)();
-        else
-            flags = 0;
+        /* Call the plugin registration function. */
+        module->register_cb();
 
         new_plug = g_new(plugin, 1);
         new_plug->handle = handle;
         new_plug->name = g_strdup(name);
-        new_plug->version = plug_version;
-        new_plug->flags = flags;
+        new_plug->module = module;
 
         /* Add it to the list of plugins. */
         g_hash_table_replace(plugins_module, new_plug->name, new_plug);
@@ -278,7 +285,8 @@ plugins_get_descriptions(plugin_description_callback callback, void *callback_da
 
     for (unsigned i = 0; i < plugins_array->len; i++) {
         plugin *plug = (plugin *)plugins_array->pdata[i];
-        callback(plug->name, plug->version, plug->flags, g_module_name(plug->handle), callback_data);
+        callback(plug->name, plug->module->version, plug->module->flags,
+                    g_module_name(plug->handle), callback_data);
     }
 
     g_ptr_array_free(plugins_array, true);
@@ -325,6 +333,18 @@ plugins_supported(void)
     return g_module_supported();
 }
 
+int
+plugins_abi_version(plugin_type_e type)
+{
+    switch (type) {
+        case WS_PLUGIN_EPAN:    return WIRESHARK_ABI_VERSION_EPAN;
+        case WS_PLUGIN_WIRETAP: return WIRESHARK_ABI_VERSION_WIRETAP;
+        case WS_PLUGIN_CODEC:   return WIRESHARK_ABI_VERSION_CODEC;
+        default: return -1;
+    }
+    ws_assert_not_reached();
+}
+
 /*
  * Editor modelines
  *
index 113f1f687dd756181a8207d9b527a34242113db0..fa4768f6105ace78710eb261402274bb336c77e8 100644 (file)
 extern "C" {
 #endif /* __cplusplus */
 
-typedef void (*plugin_register_func)(void);
-typedef uint32_t (*plugin_describe_func)(void);
-
-typedef void plugins_t;
-
 typedef enum {
     WS_PLUGIN_EPAN,
     WS_PLUGIN_WIRETAP,
     WS_PLUGIN_CODEC
 } plugin_type_e;
 
+typedef enum {
+    /* Plug-in license is GPLv2-or-later */
+    WS_PLUGIN_IS_GPLv2_OR_LATER     = 0x2222,     /* Ok */
+    /* Plug-in license is compatible with the GPL version 2, according to the FSF. */
+    /* https://www.gnu.org/licenses/gpl-faq.html#WhatDoesCompatMean */
+    WS_PLUGIN_IS_GPLv2_COMPATIBLE   = 0x2002,     /* Ok */
+    /* Plug-in license is none of the above */
+    WS_PLUGIN_IS_GPLv2_INCOMPATIBLE = 0,          /* Not allowed, will refuse to load.*/
+} plugin_license_e;
+
+#define WS_PLUGIN_SPDX_GPLv2    "GPL-2.0-or-later"
+#define WS_PLUGIN_GITLAB_URL    "https://gitlab.com/wireshark/wireshark"
+
 #define WS_PLUGIN_DESC_DISSECTOR    (1UL << 0)
 #define WS_PLUGIN_DESC_FILE_TYPE    (1UL << 1)
 #define WS_PLUGIN_DESC_CODEC        (1UL << 2)
@@ -35,6 +43,23 @@ typedef enum {
 #define WS_PLUGIN_DESC_TAP_LISTENER (1UL << 4)
 #define WS_PLUGIN_DESC_DFILTER      (1UL << 5)
 
+typedef void plugins_t;
+
+typedef void (*module_register_func)(void);
+
+struct ws_module {
+    plugin_license_e license;
+    int min_api_level; /* unused */
+    uint32_t flags;
+    const char *version;
+    const char *spdx_id;
+    const char *home_url;
+    const char *blurb;
+    module_register_func register_cb;
+};
+
+typedef plugin_type_e (*ws_load_module_func)(int *, int *, struct ws_module **);
+
 WS_DLL_PUBLIC plugins_t *plugins_init(plugin_type_e type);
 
 typedef void (*plugin_description_callback)(const char *name, const char *version,
@@ -51,6 +76,29 @@ WS_DLL_PUBLIC void plugins_cleanup(plugins_t *plugins);
 
 WS_DLL_PUBLIC bool plugins_supported(void);
 
+WS_DLL_PUBLIC
+int plugins_abi_version(plugin_type_e type);
+
+#define WIRESHARK_PLUGIN_REGISTER(type, ptr_, api_level_) \
+    WS_DLL_PUBLIC plugin_type_e \
+    wireshark_load_module(int *abi_version, int *min_api_level, \
+                            struct ws_module **plug_ptr) \
+    { \
+        *abi_version = WIRESHARK_ABI_VERSION_ ## type; \
+        *min_api_level = api_level_; \
+        *plug_ptr = ptr_; \
+        return WS_PLUGIN_ ## type; \
+    }
+
+#define WIRESHARK_PLUGIN_REGISTER_EPAN(ptr, level) \
+    WIRESHARK_PLUGIN_REGISTER(EPAN, ptr, level)
+
+#define WIRESHARK_PLUGIN_REGISTER_WIRETAP(ptr, level) \
+    WIRESHARK_PLUGIN_REGISTER(WIRETAP, ptr, level)
+
+#define WIRESHARK_PLUGIN_REGISTER_CODEC(ptr, level) \
+    WIRESHARK_PLUGIN_REGISTER(CODEC, ptr, level)
+
 #ifdef __cplusplus
 }
 #endif /* __cplusplus */