[Spice-devel] [PATCH 1/2] Ensure that plugins cannot bypass version check
Lukáš Hrázký
lhrazky at redhat.com
Fri Mar 23 13:53:08 UTC 2018
On Fri, 2018-03-23 at 13:05 +0100, Christophe de Dinechin wrote:
> From: Christophe de Dinechin <dinechin at redhat.com>
>
> This change addresses three issues related to plugin version checking:
>
> 1. It is possible for plugins to bypass version checking or do it wrong
> (as a matter of fact, the mjpeg fallback sets a bad example)
>
> 2. The current plugin version check violates the C++ ODR, i.e.
> it relies on undefined behaviors when the header used to compile
> the plugin and to compile the agent are not identical,
>
> 3. A major.minor numbering scheme is not ideal for ABI checks.
> In particular, it makes it difficult to react to an incompatibility
> that was detected post release.
>
> [More info]
>
> 1. Make it impossible to bypass version checking
>
> The current code depends on the plugin implementing the version check
> correctly by calling PluginVersionIsCompatible. To make things worse,
> the only publicly available example gets this wrong and uses an
> ad-hoc version check, so anybody copy-pasting this code will get it
> wrong.
>
> It is more robust to do the version check in the agent before calling
> any method in the plugin. It ensures that version checking cannot be
> bypassed, is done consistently and generates consistent error messages.
>
> As an indication that the aproach is robust, the new check correctly
> refuses to load older plugins that use the old version checking method:
>
> spice-streaming-agent[5167]:
> error loading plugin [...].so: no version information
>
> 2. ODR-related problems
>
> The C++ One Definition Rule (ODR) states that all translation units
> must see the same definitions. In the current code, when we call
> Agent::PluginVersionIsCompatible from the plugin, it is an ODR
> violation as soon as we have made any change in the Agent class
> compared to what the plugin was compiled against.
>
> The current code therefore relies on implementation dependent knowlege
> of how virtual functions are laid out in the vtable, and puts
> unnecessary constraints on the changes allowed in the classes
> (e.g. it's not allowed to put anything before some member functions)
>
> 3. Major.minor numbering scheme
>
> The major.minor numbering scheme initially selected makes it harder
> to fixes cases where an incompatibility was detected after release.
>
> For example, the major.minor version checking assumes that agent 1.21
> is compatible with plugins 1.21, 1.13 or 1.03. If later testing
> shows that 1.13 actually introduced an incompatiliy, you have to
> special-case 1.13 in the compatibiliy check.
>
> An approach that does not have this problem is to rely on incremented
> version numbers, with a "current" and "oldest compatible" version
> number. This is used for example by libtools [1].
>
> Since the change required for #1 and #2 introduces an ABI break,
> it is as good a time as any to also change the numbering scheme,
> since changing it later would introduce another unnecessary ABI break.
Great! AFAIK we haven't made a release yet so we don't need to worry
about ABI breakage yet?
> [1] https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html
>
> Signed-off-by: Christophe de Dinechin <dinechin at redhat.com>
> ---
> include/spice-streaming-agent/plugin.hpp | 50 +++++++++++++++++---------------
> src/concrete-agent.cpp | 35 +++++++++++-----------
> src/concrete-agent.hpp | 4 ---
> src/mjpeg-fallback.cpp | 3 --
> 4 files changed, 45 insertions(+), 47 deletions(-)
>
> diff --git a/include/spice-streaming-agent/plugin.hpp b/include/spice-streaming-agent/plugin.hpp
> index e08e3a6..0ec5040 100644
> --- a/include/spice-streaming-agent/plugin.hpp
> +++ b/include/spice-streaming-agent/plugin.hpp
> @@ -23,11 +23,22 @@ namespace streaming_agent {
> class FrameCapture;
>
> /*!
> - * Plugin version, only using few bits, schema is 0xMMmm
> - * where MM is major and mm is the minor, can be easily expanded
> - * using more bits in the future
> + * Plugins use a versioning system similar to that implemented by libtool
> + * http://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html
> + * Update the version information as follows:
> + * [ANY CHANGE] If any interfaces have been added, removed, or changed since the last update,
> + * increment PluginInterfaceVersion.
> + * [COMPATIBLE CHANGE] If interfaces have only been added since the last public release,
> + * leave PluginInterfaceOldestCompatibleVersion identical.
> + * [INCOMPATIBLE CHANGE] If any interfaces have been removed or changed since the last release,
> + * set PluginInterfaceOldestCompatibleVersion to PluginInterfaceVersion.
> + * [DETECTED INCOMPATIBILITY]: If an incompatibility is detected after a release,
> + * set PluginInterfaceOldestCompatibleVersion to the last known compatible version.
> */
> -enum Constants : unsigned { PluginVersion = 0x100u };
> +enum Constants : unsigned {
> + PluginInterfaceVersion = 1,
> + PluginInterfaceOldestCompatibleVersion = 1
> +};
This is still not too pretty, consider at least renaming Constants to
something better, or even use something like:
struct PluginInterfaceVersion {
static constexpr uint16_t current = 1;
static constexpr uint16_t oldest_compatible = 1;
};
For the encapsulation?
Cheers,
Lukas
> enum Ranks : unsigned {
> /// this plugin should not be used
> @@ -103,20 +114,6 @@ class Agent
> {
> public:
> /*!
> - * Get agent version.
> - * Plugin should check the version for compatibility before doing
> - * everything.
> - * \return version specified like PluginVersion
> - */
> - virtual unsigned Version() const = 0;
> -
> - /*!
> - * Check if a given plugin version is compatible with this agent
> - * \return true is compatible
> - */
> - virtual bool PluginVersionIsCompatible(unsigned pluginVersion) const = 0;
> -
> - /*!
> * Register a plugin in the system.
> */
> virtual void Register(Plugin& plugin) = 0;
> @@ -136,18 +133,25 @@ typedef bool PluginInitFunc(spice::streaming_agent::Agent* agent);
>
> #ifndef SPICE_STREAMING_AGENT_PROGRAM
> /*!
> + * Plugin interface version
> + * Each plugin should define this variable and set it to PluginInterfaceVersion
> + * That version will be checked by the agent before executing any plugin code
> + */
> +extern "C" unsigned spice_streaming_agent_plugin_interface_version;
> +
> +/*!
> * Plugin main entry point.
> - * Plugins should check if the version of the agent is compatible.
> - * If is compatible should register itself to the agent and return
> - * true.
> - * If is not compatible can decide to stay in memory or not returning
> - * true (do not unload) or false (safe to unload). This is necessary
> + * This entry point is only called if the version check passed.
> + * It should return true if it loaded and initialized successfully.
> + * If the plugin does not initialize and does not want to be unloaded,
> + * it may still return true on failure. This is necessary
> * if the plugin uses some library which are not safe to be unloaded.
> * This public interface is also designed to avoid exporting data from
> * the plugin which could be a problem in some systems.
> * \return true if plugin should stay loaded, false otherwise
> */
> extern "C" spice::streaming_agent::PluginInitFunc spice_streaming_agent_plugin_init;
> +
> #endif
>
> #endif // SPICE_STREAMING_AGENT_PLUGIN_HPP
> diff --git a/src/concrete-agent.cpp b/src/concrete-agent.cpp
> index 4cf70e7..eb4f333 100644
> --- a/src/concrete-agent.cpp
> +++ b/src/concrete-agent.cpp
> @@ -15,28 +15,11 @@
>
> using namespace spice::streaming_agent;
>
> -static inline unsigned MajorVersion(unsigned version)
> -{
> - return version >> 8;
> -}
> -
> -static inline unsigned MinorVersion(unsigned version)
> -{
> - return version & 0xffu;
> -}
> -
> ConcreteAgent::ConcreteAgent()
> {
> options.push_back(ConcreteConfigureOption(nullptr, nullptr));
> }
>
> -bool ConcreteAgent::PluginVersionIsCompatible(unsigned pluginVersion) const
> -{
> - unsigned version = Version();
> - return MajorVersion(version) == MajorVersion(pluginVersion) &&
> - MinorVersion(version) >= MinorVersion(pluginVersion);
> -}
> -
> void ConcreteAgent::Register(Plugin& plugin)
> {
> plugins.push_back(std::shared_ptr<Plugin>(&plugin));
> @@ -83,6 +66,24 @@ void ConcreteAgent::LoadPlugin(const std::string &plugin_filename)
> return;
> }
>
> + unsigned *version =
> + (unsigned *) dlsym(dl, "spice_streaming_agent_plugin_interface_version");
> + if (!version) {
> + syslog(LOG_ERR, "error loading plugin %s: no version information",
> + plugin_filename.c_str());
> + return;
> + }
> + if (*version < PluginInterfaceOldestCompatibleVersion ||
> + *version > PluginInterfaceVersion) {
> + syslog(LOG_ERR,
> + "error loading plugin %s: plugin interface version %u, "
> + "agent accepts version %u...%u",
> + plugin_filename.c_str(), *version,
> + PluginInterfaceOldestCompatibleVersion,
> + PluginInterfaceVersion);
> + return;
> + }
> +
> try {
> PluginInitFunc* init_func =
> (PluginInitFunc *) dlsym(dl, "spice_streaming_agent_plugin_init");
> diff --git a/src/concrete-agent.hpp b/src/concrete-agent.hpp
> index 5bca23b..c631916 100644
> --- a/src/concrete-agent.hpp
> +++ b/src/concrete-agent.hpp
> @@ -27,16 +27,12 @@ class ConcreteAgent final : public Agent
> {
> public:
> ConcreteAgent();
> - unsigned Version() const override {
> - return PluginVersion;
> - }
> void Register(Plugin& plugin) override;
> const ConfigureOption* Options() const override;
> void LoadPlugins(const std::string &directory);
> // pointer must remain valid
> void AddOption(const char *name, const char *value);
> FrameCapture *GetBestFrameCapture(const std::set<SpiceVideoCodecType>& codecs);
> - bool PluginVersionIsCompatible(unsigned pluginVersion) const override;
> private:
> void LoadPlugin(const std::string &plugin_filename);
> std::vector<std::shared_ptr<Plugin>> plugins;
> diff --git a/src/mjpeg-fallback.cpp b/src/mjpeg-fallback.cpp
> index 68c282f..605a4b3 100644
> --- a/src/mjpeg-fallback.cpp
> +++ b/src/mjpeg-fallback.cpp
> @@ -180,9 +180,6 @@ SpiceVideoCodecType MjpegPlugin::VideoCodecType() const {
>
> bool MjpegPlugin::Register(Agent* agent)
> {
> - if (agent->Version() != PluginVersion)
> - return false;
> -
> std::unique_ptr<MjpegPlugin> plugin(new MjpegPlugin());
>
> try {
> --
> 2.13.5 (Apple Git-94)
> _______________________________________________
> Spice-devel mailing list
> Spice-devel at lists.freedesktop.org
> https://lists.freedesktop.org/mailman/listinfo/spice-devel
More information about the Spice-devel
mailing list