Version and Platform (required):
- Binary Ninja Version: 5.4.10102-dev Ultimate (07215472)
- Edition: Ultimate
- OS: macOS
- OS Version: 26.5.1
- CPU Architecture: arm64 (Apple Silicon)
Bug Description:
I have a set of plugins that link against the shared cache plugin, and libsharedcache.dylib was being initialised three times on startup, leaving three registered DSCView binary view types. None of my dylibs call anything in the shared cache plugin's initialisation.
Binary Ninja resolves plugin entry points by calling dlsym() on the dlopen() handle of each dylib in the plugins directory. On both macOS and Linux, dlsym() with a handle searches that object and its dependencies, not just the object itself. So a dylib that links against another plugin inherits that plugin's CorePluginInit, and Binary Ninja calls it a second time.
The consequence is that any plugin — or any support dylib placed in the plugins directory — that links another plugin silently re-initialises that plugin, without defining CorePluginInit at all.
Linking libsharedcache.dylib is the case where this really bites, because its CorePluginInit calls SharedCacheViewType::Register() and RegisterSharedCacheWorkflow(). Each extra call registers another DSCView binary view type and another workflow, so a shared cache file is offered several identical-looking view types, and each one that gets used parses the cache again. Anything linking the shared cache API hits this simply by being installed in the plugins directory.
Steps To Reproduce:
dep_plugin.c — a normal core plugin that other plugins link against (this is the role libsharedcache.dylib plays in the real case):
#include <stdint.h>
#include <stdio.h>
#include "binaryninjacore.h"
extern "C" {
BN_DECLARE_CORE_ABI_VERSION
/* Something a consumer calls, which is what creates the link dependency. */
BINARYNINJAPLUGIN int dep_helper(void) { return 42; }
BINARYNINJAPLUGIN int CorePluginInit(void)
{
static int callCount = 0;
fprintf(stderr, "[poc] dep_plugin CorePluginInit called %d time(s)\n", ++callCount);
return 1;
}
};
consumer.c — links dep_plugin and defines no plugin entry points whatsoever:
#include "binaryninjacore.h"
extern int dep_helper(void);
extern "C" {
BINARYNINJAPLUGIN int consumer_entry(void) { return dep_helper(); }
};
Build both and copy them into the user plugins directory. Then start Binary Ninja (the GUI shows the same thing; headless is easier to capture):
python3 -c "import binaryninja; binaryninja._init_plugins()"
Observed output:
[poc] dep_plugin CorePluginInit called 1 time(s)
[0:0 Default info] Loaded plugin libpoc_consumer
[poc] dep_plugin CorePluginInit called 2 time(s)
[0:0 Default info] Loaded plugin libpoc_dep
dep_plugin's CorePluginInit runs twice, and the first of those runs is attributed to libpoc_consumer — a dylib that does not define it.
man dlsym documents this: the handle's image and any libraries it depends on are searched.
Expected Behavior:
CorePluginInit should be called only when the dylib being loaded actually defines it (and should probably only ever be called once anyway?). A symbol inherited from a dependency should be ignored, so that linking against another plugin does not re-initialise it.
dladdr() on the resolved symbol gives an easy check — if the defining image is not the dylib being loaded, treat the plugin as having no CorePluginInit:
void* sym = dlsym(handle, "CorePluginInit");
Dl_info info;
if (sym && dladdr(sym, &info) && paths_refer_to_same_file(info.dli_fname, pluginPath))
{
/* genuinely this plugin's entry point */
}
The same check applies to CorePluginABIVersion, CorePluginDependencies, and the UIPlugin* entry points.
Additional Information:
This bites hardest for native UI plugins, because a UI plugin has to live directly in the plugins directory and there is no way to stop dlsym from reaching into its dependency tree — dlsym searches the full transitive closure, so removing the direct link does not help if the dependency is reached through an intermediate library.
The workaround is for the UI plugin to define its own CorePluginInit that returns false, which shadows the inherited one.
The duplicate initialisation stops, and returning false does not prevent UIPluginInit from running, so the UI plugin still works. That this is necessary at all is surprising, and the native UI plugin example does not include it — a native UI plugin that links any plugin is silently broken in this way until its author works out what is happening.
A support library that is not a plugin can dodge the problem by being installed in a subdirectory of the plugins directory instead of directly inside it, since only dylibs sitting directly in the plugins directory are probed. A UI plugin has no equivalent escape.
Co-authored by Claude (Anthropic), which reproduced the behaviour and wrote up this report. I have read and edited the report.
Version and Platform (required):
Bug Description:
I have a set of plugins that link against the shared cache plugin, and
libsharedcache.dylibwas being initialised three times on startup, leaving three registeredDSCViewbinary view types. None of my dylibs call anything in the shared cache plugin's initialisation.Binary Ninja resolves plugin entry points by calling
dlsym()on thedlopen()handle of each dylib in the plugins directory. On both macOS and Linux,dlsym()with a handle searches that object and its dependencies, not just the object itself. So a dylib that links against another plugin inherits that plugin'sCorePluginInit, and Binary Ninja calls it a second time.The consequence is that any plugin — or any support dylib placed in the plugins directory — that links another plugin silently re-initialises that plugin, without defining
CorePluginInitat all.Linking
libsharedcache.dylibis the case where this really bites, because itsCorePluginInitcallsSharedCacheViewType::Register()andRegisterSharedCacheWorkflow(). Each extra call registers anotherDSCViewbinary view type and another workflow, so a shared cache file is offered several identical-looking view types, and each one that gets used parses the cache again. Anything linking the shared cache API hits this simply by being installed in the plugins directory.Steps To Reproduce:
dep_plugin.c— a normal core plugin that other plugins link against (this is the rolelibsharedcache.dylibplays in the real case):consumer.c— linksdep_pluginand defines no plugin entry points whatsoever:Build both and copy them into the user plugins directory. Then start Binary Ninja (the GUI shows the same thing; headless is easier to capture):
Observed output:
dep_plugin'sCorePluginInitruns twice, and the first of those runs is attributed tolibpoc_consumer— a dylib that does not define it.man dlsymdocuments this: the handle's image and any libraries it depends on are searched.Expected Behavior:
CorePluginInitshould be called only when the dylib being loaded actually defines it (and should probably only ever be called once anyway?). A symbol inherited from a dependency should be ignored, so that linking against another plugin does not re-initialise it.dladdr()on the resolved symbol gives an easy check — if the defining image is not the dylib being loaded, treat the plugin as having noCorePluginInit:The same check applies to
CorePluginABIVersion,CorePluginDependencies, and theUIPlugin*entry points.Additional Information:
This bites hardest for native UI plugins, because a UI plugin has to live directly in the plugins directory and there is no way to stop
dlsymfrom reaching into its dependency tree —dlsymsearches the full transitive closure, so removing the direct link does not help if the dependency is reached through an intermediate library.The workaround is for the UI plugin to define its own
CorePluginInitthat returnsfalse, which shadows the inherited one.The duplicate initialisation stops, and returning
falsedoes not preventUIPluginInitfrom running, so the UI plugin still works. That this is necessary at all is surprising, and the native UI plugin example does not include it — a native UI plugin that links any plugin is silently broken in this way until its author works out what is happening.A support library that is not a plugin can dodge the problem by being installed in a subdirectory of the plugins directory instead of directly inside it, since only dylibs sitting directly in the plugins directory are probed. A UI plugin has no equivalent escape.
Co-authored by Claude (Anthropic), which reproduced the behaviour and wrote up this report. I have read and edited the report.