Difference between revisions of "User:TomYu/Plugin support improvements"
(example PLM header) |
|||
Line 2: | Line 2: | ||
{{project-early}} |
{{project-early}} |
||
+ | |||
+ | The MIT Kerberos implementation has a number of places where modular extensibility would facilitate development and deployment of new technologies. Developers of the core code and authors of extension code have an interest in making it easier for third parties to extend the code in a well-compartmentalized way. Administrators of Kerberos deployments and packagers of software have an interest in making it easy to deploy new capabilities in a simple way. |
||
== Priorities == |
== Priorities == |
||
Line 11: | Line 13: | ||
# Improve readability of code that calls pluggable interfaces |
# Improve readability of code that calls pluggable interfaces |
||
# Allow easier creation of new pluggable interfaces |
# Allow easier creation of new pluggable interfaces |
||
+ | # Allow incremental transition of existing pluggable interfaces to the new framework |
||
== Deliverables for release 1.9 == |
== Deliverables for release 1.9 == |
||
Line 18: | Line 21: | ||
== Requirements == |
== Requirements == |
||
+ | * thread safety |
||
* minimal configuration required |
* minimal configuration required |
||
− | :* no explicit configuration |
+ | :* no explicit configuration needed to use built-in modules in their default modes |
− | :* no explicit configuration |
+ | :* no explicit configuration needed to use loadable modules if the files containing them are located in the default directory for that kind of module |
+ | * allow changing of default directory base for loadable plugin modules of all pluggable interfaces |
||
+ | * any resources allocated by a plugin module must be released by that module and by no other code |
||
− | == |
+ | == Components == |
− | ; pluggable interface: an (internal) interface that can be implemented by a third party. These can be one-to-one, or one-to-many. An example of one-to-one is the DAL, and an example of one-to-many is preauth. |
||
+ | * pluggable interface (PLIF) |
||
+ | ** caller (consumer) interface |
||
+ | ** provider interface |
||
+ | * plugin manager (PLM) |
||
+ | * plugin registry (PLR) |
||
− | ; module: a unit of code that implements a pluggable interface. It can be built in, or it can be dynamically loadable. |
||
+ | === Pluggable interface === |
||
− | :; built-in: a module whose executable code is located within the library shared object or executable program file, or behaves as if it were. (Dependent library shared objects of the calling library can contain "built-in" modules for the calling library, but this can cause problems with cyclic references.) The distinguishing characteristic of a built-in module is that, as part of program startup, the operating system automatically maps the executable code of the module into the address space of the process that calls it, without any explicit action by the library or program. |
||
− | :; dynamically loaded: a module whose executable code is located within a file that is distinct from the library or program that calls it. The plugin support framework uses the runtime linker (or equivalent) to explicitly map the executable code of the module into the process address space. |
||
− | ; discovery: process of enumerating what modules are available for a pluggable interface. Includes possible filtering of the raw discovered set. |
||
+ | A pluggable interface is an interface, possibly internal to a library, that can be implemented by a third party in a modular, well-compartmentalized manner. These implementations of pluggable interfaces are called plugin modules. Pluggable interfaces allow a consumer to use the capabilities of the interface without needing to be aware of the implementation details. In particular, a pluggable interface prevents the consumer from needing to know whether the module is a built-in or a dynamically loadable module. Pluggable interfaces can be one-to-one, or one-to-many. An example of one-to-one is the DAL, and an example of one-to-many is preauth. |
||
− | :* compiled-in |
||
− | :* directory scan |
||
− | :* explicit inclusion by configuration |
||
− | :* explicit exclusion by configuration |
||
− | ; loading: the process of making modules available for calling. This can involve dynamically loading a module using the runtime linker, or it can involve registering a vtable provided by an application. |
||
+ | A pluggable interface has two parts: a consumer interface and a provider interface. Typically, library code implements the consumer interface, and application code or other library code calls the functions of the consumer interface. The provider interface is what the plugin module implements. |
||
− | :* built-in |
||
− | :* dynamic loading |
||
− | :* application-registered |
||
− | ; selection: the process of a caller invoking one specific module from the set of loaded modules that implement an interface. |
||
+ | ==== Caller (consumer) interface ==== |
||
− | ; consumer interface: the interface that a caller uses to access the services of a pluggable interface. Typically, but not always, the krb5 library implements the consumer interface. |
||
+ | The consumer interface isolates the consumer from implementation details of the pluggable interface. The caller does not generally need to know about whether a given module is built-in or dynamically loaded. The implementation of a consumer interface is essentially a glue layer, and can make use of domain-independent (not specific to any pluggable interface) capabilities of the plugin framework. The consumer might explicitly register a new module that it implements: this capability is part of a plugin registry. |
||
− | ; provider interface: the interface that a module author implements |
||
+ | A consumer of a pluggable interface uses an opaque handle, obtained from a loader function that is part of the pluggable interface, to call the methods of a plugin module. Each method of the consumer interface is an ordinary C function that takes the opaque handle either explicitly as its first argument or implicitly by some means such as a module name. Conceptually, these pluggable interface functions are wrapper functions that call through function pointers contained in the opaque plugin module handle object. (though this is not the only possible implementation) |
||
− | == Components == |
||
+ | A handle can represent: |
||
− | * plugin |
+ | * the plugin module itself |
− | * plugin |
+ | * a resource to which the plugin module provides access (e.g., a ccache handle) |
− | * |
+ | * a set of plugin modules (e.g., the set of all available preauth mechanisms) |
− | ** caller (consumer) interface |
||
− | ** provider interface |
||
− | === Plugin manager === |
||
+ | One rationale for using wrapper functions instead of having the caller directly invoke methods through a function pointer is to make it easier for debuggers and analysis tools to recognize when a particular interface method is being called. (Function pointers might have identifier names that look nothing like the actual name of the function they point to, in addition to enabling confusing aliasing.) |
||
− | The plugin manager (PLM) provides a set of generic support capabilities that are independent of individual pluggable interfaces. It centralizes the discovery process for plugin modules. Typically, consumers of pluggable interfaces do not call it directly, but instead call a loader function of the pluggable interface. |
||
+ | The loader function is specific to the pluggable interface. One reason is for type safety: there will be a distinct opaque handle type for each pluggable interface, allowing compile-time checking to catch some sorts of programming errors. Another reason is backward compatibility: it allows a pluggable interface to support plugin modules that implement an older provider interface. |
||
− | <pre> |
||
+ | ==== Provider interface ==== |
||
− | /** opaque handle for plugin manager context */ |
||
− | typedef struct k5_plm_ctx_ *k5_plm_ctx; |
||
− | /** opaque handle for a single plugin module */ |
||
− | typedef struct k5_pmod_ *k5_pmod; |
||
− | /** opaque handle for a set of plugin modules */ |
||
− | typedef struct k5_pmod_set_ *k5_pmod_set; |
||
− | /** numeric ID designating a pluggable interface */ |
||
+ | A plugin module is a unit of code that implements the provider interface portion of a pluggable interface. Plugin modules can be built in or dynamically loaded. Several alternatives exist for the form of the provider interface, but some have significant advantages in allowing the plugin module to use identical source code for both built-in and loadable modules. |
||
− | typedef int k5_plif; |
||
− | #define K5_PLIF_CCACHE 1 |
||
+ | A built-in module is a module whose executable code is located within the library shared object or executable program file, or behaves as if it were. (While separate library shared objects that the calling library depends on can contain "built-in" modules for the calling library, this can cause problems with cyclic references.) The distinguishing characteristic of a built-in module is that, as part of program startup, the operating system automatically maps the executable code of the module into the address space of the process that calls it, without any explicit action by the library or program. |
||
− | #define K5_PLIF_RCACHE 2 |
||
− | #define K5_PLIF_PWQUAL 3 |
||
− | /** Initialize the plugin manager. */ |
||
+ | A dynamically loaded module is a module whose executable code is located within a file that is distinct from the library or program that calls it. The plugin support framework uses the runtime linker (or equivalent) to explicitly map the executable code of the module into the process address space. In POSIX systems, this is typically done using <code>dlopen()</code>. |
||
− | krb5_error_code k5_plm_init(k5_plm_ctx *ctx); |
||
− | /** Shut down the plugin manager. */ |
||
+ | ===== Loadable module provider interface ===== |
||
− | void k5_plm_fini(k5_plm_ctx ctx); |
||
− | /** Retrieve a module handle for the named module of the specified type. */ |
||
+ | A loadable module exports a single function symbol, which denotes the vtable constructor function for that module. This function takes as arguments an interface version number, a pointer to a caller-allocated vtable structure for the interface, and the size of the structure (as an added precaution against version mismatches). |
||
− | krb5_error_code k5_plm_get_pmod(k5_plm_ctx ctx, k5_plif plif, |
||
− | const char *name, k5_pmod *pmod); |
||
− | /** Free a plugin module handle. */ |
||
+ | Although the caller (actually the plugin support code) allocates the vtable structure in the above description, one alternative is to have the module perform the allocation of the structure itself. This can cause problems if the module uses a different memory allocator than the caller. |
||
− | void k5_plm_free_pmod(k5_plm_ctx ctx, k5_pmod pmod); |
||
− | /** Retrieve a handle for a set of modules |
||
+ | Should the single function symbol have a prefix that depends on the name of the plugin module? |
||
− | of the specified pluggable interface. */ |
||
− | krb5_error_code k5_plm_get_pmod_set(k5_plm_ctx ctx, k5_plif plif, |
||
− | k5_pmod_set *pmset); |
||
− | /** Free a module set. */ |
||
+ | ;Yes: allows identical module source code to be used for the built-in and loadable module versions of a module |
||
− | void k5_plm_free_pmod_set(k5_plm_ctx ctx, k5_pmod_set pmset); |
||
+ | ;No: adding a prefix to the symbol adds complexity to the code that looks up the symbol |
||
− | /** Get a data symbol from a module. */ |
||
+ | ===== Built-in module provider interface ===== |
||
− | krb5_error_code k5_plm_get_data(k5_plm_ctx ctx, k5_pmod pmod, const char *name, |
||
− | void **data); |
||
− | /** Get a function symbol from a module. */ |
||
+ | A built-in module provides the same interface as a loadable module. In the alternative where we use same exported function symbol for each loadable module implementing a pluggable interface, the built-in modules will still need distinct prefixes are required for each vtable retrieval function. This requires slightly different source code for a built-in module versus a loadable module. |
||
− | krb5_error_code k5_plm_get_func(k5_plm_ctx ctx, k5_pmod pmod, const char *name, |
||
− | void (**func)()); |
||
− | /** Get a set of function symbols from a module set. */ |
||
+ | ===== Alternative: one symbol per method ===== |
||
− | krb5_error_code k5_plm_get_data_set(k5_plm_ctx ctx, k5_pmod pmod, |
||
− | const char *name, |
||
− | void ***data, size_t *count); |
||
− | /** Get a set of function symbols from a module set. */ |
||
+ | Exporting one symbol per method in a loadable module is an alternative that might require less effort from a module author, as it does not require that a vtable be part of the provider interface. Built-in modules would probably still require a vtable (though that could be provided by the implementation of the consumer interface). |
||
− | krb5_error_code k5_plm_get_func_set(k5_plm_ctx ctx, k5_pmod pmod, |
||
− | const char *name, |
||
− | void (***func)(), size_t *count); |
||
− | </pre> |
||
− | * given the identifier of a pluggable interface, return an opaque handle representing the entire collection of available plugin modules that implements that pluggable interface. |
||
+ | ===== Alternative: loader with fixed module set ===== |
||
− | * given the identifier of a pluggable interface and a module name, return an opaque generic handle for that module. For loadable modules, this can contain the handle from <code>dlopen()</code>. For built-in modules, this can contain a pointer to a vtable. |
||
+ | This approach uses a slightly different definition for "loader". A loader is a function that acts like a plugin registry that knows about a fixed, predetermined set of plugin modules. Each module in this set may implement a different pluggable interface. Typically, a built-in loader function knows about all of the built-in modules, regardless of what interface each module implements. A collection of modules (possibly implementing different pluggable interfaces) can be combined in a single dynamically loaded shared object that only exports the loader function interface. To search for a dynamically loaded plugin module, the plugin framework would load each of these shared objects and call the loader function of each one until it located the desired module. |
||
− | === Plugin |
+ | === Plugin manager === |
− | The plugin loader (PLL) provides generic utility functions for loading plugin modules. |
||
+ | The plugin manager (PLM) provides a set of generic support capabilities that are independent of individual pluggable interfaces. It centralizes the discovery process for plugin modules. Typically, consumers of pluggable interfaces do not call it directly, but instead call a loader function of the pluggable interface, which in turn calls the plugin manager. The plugin manager works in concert with a set of plugin registries to locate plugin modules. |
||
− | === Caller (consumer) interface === |
||
+ | The <code>krb5_init_context()</code> functions will create a plugin manager context that will exist in the krb5_context, and add the initial plugin registries to the plugin manager context. An application may use <code>krb5_get_plm()</code> to retrieve the plugin manager context so it can register its own built-in plugin modules. |
||
− | A caller (consumer) of a pluggable interface uses an opaque handle to call the methods of a plugin module. Each method of the consumer interface is an ordinary C function that takes the opaque handle either explicitly as its first argument or implicitly by some means such as a module name. Conceptually, these pluggable interface functions are wrapper functions that call through function pointers contained in the opaque handle object. (though this is not the only possible implementation) |
||
+ | The plugin manager locates plugin modules using both a numeric identifer that designates a pluggable interface and a string that names a module that implements that pluggable interface. The primary way to use the plugin manager is to query it for the vtable constructor for a specified module (or a set of vtable constructors for all modules of that interface). |
||
− | One rationale for doing this instead of having the caller invoke methods through a function pointer is that it makes it easier for debuggers and analysis tools to recognize when a particular interface method is being called. (Function pointers might have identifier names that look nothing like the actual name of the function they point to, in addition to enabling confusing aliasing.) |
||
+ | As a lower-level interface that supports backward compatibility for older loadable-module provider interfaces, the plugin manager can create an opaque locator handle for a specified module. The plugin manager also supports looking up sets of locator handles (for all modules implementing a specified pluggable interface). |
||
− | A handle can represent: |
||
+ | Additional functions in the plugin manager interface allow for lookups of symbols in the plugin module (if supported by that module) |
||
− | * the plugin module itself |
||
+ | === Plugin registry === |
||
− | * a resource to which the plugin module provides access (e.g., a ccache handle) |
||
− | * a set of plugin modules (e.g., the set of all available preauth mechanisms) |
||
− | The caller does not generally need to know about whether a given module is built-in or dynamically loaded. The caller might explicitly register a new module that it implements. |
||
+ | A plugin registry (PLR) uses pairs of plugin interface identifiers and plugin module names to look up modules. Most of the functionality of a plugin registry is only accessible indirectly through the plugin manager, but some user-level functionality exists for each type of registry, such as an interface to create a new registry or to register a new plugin module in a registry. The primary way to use a registry is to query it for the vtable constructor for a specified module. |
||
− | === Provider interface === |
||
+ | A plugin registry can also produce a opaque module locator handle for a specified module. It provides lookup functions so that a loader function of a pluggable interface can do the equivalent of <code>dlsym()</code> on the module represented by a locator handle. |
||
− | The provider interface for a built-in module will generally take the form of a structure of function pointers, each of which points to a method implementation within the module. |
||
+ | Initially, the plugin framework implementation will contain two types of registry: |
||
− | The initial loadable module provider interface will support operating systems with POSIX-style <code>dlopen()</code> capabilities. |
||
+ | ; built-in module table registry: locates built-in plugin modules. This also allows an application to register its private built-in plugin modules. |
||
− | Does each loadable plugin module implementing a pluggable interface need to prepend a prefix to the name of each method? e.g., if the method is "f1", does module "foo" name its implementation of "f1" "foo_f1"? If not, how would builtin modules work? |
||
+ | ; loadable module directories registry: locates dynamically loadable plugin modules. It maps plugin interface identifiers to directories where modules implementing that interface can be found, and can thereby locate individual modules. |
||
− | ==== Loadable module provider interface ==== |
||
+ | The <code>krb5_init_context()</code> functions will call various registry methods to create and configure registries for libkrb5 built-in modules and default directories for loadable modules. |
||
− | A loadable module exports a single function symbol. The function takes as arguments an interface version number, a pointer to a caller-allocated vtable structure for the interface, and the size of the structure (as an added precaution against version mismatches). |
||
+ | ==== Registry of built-in modules ==== |
||
− | Although the caller (actually the plugin support code) allocates the vtable structure in the above description, one alternative is to have the module perform the allocation of the structure itself. This can cause problems if the module uses a different memory allocator than the caller. |
||
+ | This kind of registry keeps track of built-in modules. Typically, libkrb5 will initialize this with locators for all of the built-in modules that are linked into it. Applications can also register private built-in plugin modules using this type of registry. '''TO BE DETERMINED:''' Are libkrb5 registry tables read-only? If they are, applications will probably need to add their own registries to the plugin manager in order to register their private built-in plugin modules. |
||
− | Should the single function symbol have a prefix that depends on the name of the plugin module? |
||
+ | ==== Registry of loadable modules ==== |
||
− | ;Yes: allows identical module source code to be used for the built-in and loadable module versions of a module |
||
+ | This kind of registry keeps track of a few additional items needed for locating loadable modules. This includes a base directory pathname to directory tree of plugin modules. Each subdirectory of the base directory has a name that corresponds to the pluggable interface identifier for the modules that intended to be in that subdirectory. |
||
− | ;No: adding a prefix to the symbol adds complexity to the code that looks up the symbol |
||
− | ==== Built-in module provider interface ==== |
||
+ | This registry type has functions to configure the mapping of pluggable interface identifiers to subdirectory names, as well as functions to configure the mapping of individual loadable module files to module names / interface identifier pairs. The <code>krb5_init_context()</code> functions will do this for the pluggable interfaces that are part of libkrb5. |
||
− | A built-in module provides the same interface as a loadable module. In the alternative where we use same exported function symbol for each loadable module implementing a pluggable interface, the built-in modules will still need distinct prefixes are required for each vtable retrieval function. |
||
+ | '''NEEDS MORE DETAIL''' |
||
− | ==== One-symbol-per-method alternative loadable module provider interface ==== |
||
+ | == Details == |
||
− | Exporting one symbol per method in a loadable module is an alternative that might require less effort from a module author, as it does not require that a vtable be part of the provider interface. Built-in modules would probably still require a vtable (though that could be provided by the implementation of the consumer interface). |
||
+ | Deallocator functions, etc. omitted for conciseness. |
||
+ | |||
+ | '''NEEDS MORE DETAIL''' |
||
+ | |||
+ | === Plugin manager details === |
||
+ | |||
+ | The following functions are meant to be used by a consumer of pluggable interfaces: |
||
+ | |||
+ | ; <code>k5_plm_init</code>: initialize a plugin manager context |
||
+ | ; <code>k5_plm_fini</code>: shut down a plugin manager context |
||
+ | ; <code>k5_plm_add_reg</code>: add a new registry to a plugin manager context |
||
+ | |||
+ | The following functions are meant to be used by a loader function of a pluggable interface: |
||
+ | |||
+ | ; <code>k5_plm_get_vtfn</code>: get a vtable constructor |
||
+ | ; <code>k5_plm_get_vtfn_set</code>: get a set of vtable constructors |
||
+ | |||
+ | The following functions are meant to be used by a loader function that needs to do lower-level operations (particularly for backward compatibility with older loadable modules). They are typically not implemented by registries that only track built-in modules: |
||
+ | |||
+ | ; <code>k5_plm_get_ploc</code>: retrieve a module locator handle |
||
+ | ; <code>k5_plm_get_ploc_set</code>: retrieve a set of module locator handles |
||
+ | ; <code>k5_ploc_get_vtfn</code>: get a vtable constructor function from a module locator |
||
+ | ; <code>k5_ploc_get_data</code>: get a data symbol from a module locator |
||
+ | ; <code>k5_ploc_get_func</code>: get a function symbol from a module locator |
||
+ | ; <code>k5_ploc_get_vtfn_set</code>: get a set of vtable constructor functions from a module locator set |
||
+ | ; <code>k5_ploc_get_data_set</code>: get a set of data symbol from a module locator set |
||
+ | ; <code>k5_ploc_get_func_set</code>: get a set of function symbol from a module locator set |
||
+ | |||
+ | ---- |
||
− | ==== Loader with fixed module set ==== |
||
+ | == Old background == |
||
− | This approach uses a slightly different definition for "loader". A loader knows about a fixed set of plugin modules, which may implement different pluggable interfaces. Typically, a built-in loader knows about all of the built-in modules, regardless of what interface each module implements. A collection of dynamically loaded modules (possibly implementing different pluggable interfaces) can be combined in a single shared object that only exports the loader interface. |
||
+ | for reference |
||
− | ==Current plugins== |
+ | === Current plugins === |
We currently have the following plugin frameworks: |
We currently have the following plugin frameworks: |
||
Line 191: | Line 187: | ||
Plugin frameworks which are "not exposed" may still be productively used by vendor forks of the krb5 tree. |
Plugin frameworks which are "not exposed" may still be productively used by vendor forks of the krb5 tree. |
||
− | ==Future plugins== |
+ | === Future plugins === |
The following areas are candidates for future plugin support: |
The following areas are candidates for future plugin support: |
||
Line 203: | Line 199: | ||
* password synchronization |
* password synchronization |
||
− | ==Current support infrastructure== |
+ | === Current support infrastructure === |
In libkrb5support, we have functions to facilitate loading plugins from shared objects. There is a set of functions to load individual plugins from named files and mechglue; these are currently used by the HDB bridge and GSS mechglue: |
In libkrb5support, we have functions to facilitate loading plugins from shared objects. There is a set of functions to load individual plugins from named files and mechglue; these are currently used by the HDB bridge and GSS mechglue: |
||
Line 221: | Line 217: | ||
* krb5int_free_plugin_dir_func - Free a list of function objects returned by krb5int_get_plugin_dir_func |
* krb5int_free_plugin_dir_func - Free a list of function objects returned by krb5int_get_plugin_dir_func |
||
− | ==Problem areas== |
+ | === Problem areas === |
* Every caller of krb5int_open_plugin_dirs specifies either no filebases (e.g. preauth plugins) or a single filebase (KDB plugins). Accepting and processing a list of filebases is probably needless complexity. |
* Every caller of krb5int_open_plugin_dirs specifies either no filebases (e.g. preauth plugins) or a single filebase (KDB plugins). Accepting and processing a list of filebases is probably needless complexity. |
||
Line 236: | Line 232: | ||
* In some scenarios such as embedded environments, it may be more useful to allow applications to supply plugin vtables via an API (as we do for keytabs and ccaches, though those APIs are not public) than to load them from shared objects in the filesystem. |
* In some scenarios such as embedded environments, it may be more useful to allow applications to supply plugin vtables via an API (as we do for keytabs and ccaches, though those APIs are not public) than to load them from shared objects in the filesystem. |
||
+ | |||
+ | == Definitions == |
||
+ | |||
+ | ; pluggable interface: an (internal) interface that can be implemented by a third party. These can be one-to-one, or one-to-many. An example of one-to-one is the DAL, and an example of one-to-many is preauth. |
||
+ | |||
+ | ; module: a unit of code that implements a pluggable interface. It can be built in, or it can be dynamically loadable. |
||
+ | :; built-in: a module whose executable code is located within the library shared object or executable program file, or behaves as if it were. (While separate library shared objects that the calling library depends on can contain "built-in" modules for the calling library, this can cause problems with cyclic references.) The distinguishing characteristic of a built-in module is that, as part of program startup, the operating system automatically maps the executable code of the module into the address space of the process that calls it, without any explicit action by the library or program. |
||
+ | :; dynamically loaded: a module whose executable code is located within a file that is distinct from the library or program that calls it. The plugin support framework uses the runtime linker (or equivalent) to explicitly map the executable code of the module into the process address space. In POSIX systems, this is typically done using <code>dlopen()</code>. |
||
+ | |||
+ | ; discovery: process of enumerating what modules are available for a pluggable interface. Includes possible filtering of the raw discovered set. |
||
+ | :* compiled-in |
||
+ | :* directory scan |
||
+ | :* explicit inclusion by configuration |
||
+ | :* explicit exclusion by configuration |
||
+ | |||
+ | ; loading: the process of making modules available for calling. This can involve dynamically loading a module using the runtime linker, or it can involve registering a vtable provided by an application. |
||
+ | :* built-in |
||
+ | :* dynamic loading |
||
+ | :* application-registered |
||
+ | |||
+ | ; selection: the process of a caller invoking one specific module from the set of loaded modules that implement an interface. |
||
+ | |||
+ | ; consumer interface: the interface that a caller uses to access the services of a pluggable interface. Typically, but not always, the krb5 library implements the consumer interface. |
||
+ | |||
+ | ; provider interface: the interface that a module author implements |
||
== Links == |
== Links == |
Revision as of 16:40, 7 July 2010
The MIT Kerberos implementation has a number of places where modular extensibility would facilitate development and deployment of new technologies. Developers of the core code and authors of extension code have an interest in making it easier for third parties to extend the code in a well-compartmentalized way. Administrators of Kerberos deployments and packagers of software have an interest in making it easy to deploy new capabilities in a simple way.
Contents
Priorities
- Allow third parties to implement multiple plugin modules for each pluggable interface
- Allow a plugin module to build as dynamic or built-in from the same source code
- Allow third parties to more easily create new plugin modules
- Provide a uniform method for configuring discovery of plugin modules
- Improve readability of code that calls pluggable interfaces
- Allow easier creation of new pluggable interfaces
- Allow incremental transition of existing pluggable interfaces to the new framework
Deliverables for release 1.9
Create a plugin framework and pluggable interfaces that can support password strength and password synchronization plugin modules. These should support the capabilities of two existing extensions written by Russ Allbery -- krb5-strength and krb5-sync. The framework is subject to change in the future, so it doesn't have to accommmodate all eventualities, but we will have a goal of not painting ourselves into a corner with respect to reasonably plausible future requirements.
Requirements
- thread safety
- minimal configuration required
- no explicit configuration needed to use built-in modules in their default modes
- no explicit configuration needed to use loadable modules if the files containing them are located in the default directory for that kind of module
- allow changing of default directory base for loadable plugin modules of all pluggable interfaces
- any resources allocated by a plugin module must be released by that module and by no other code
Components
- pluggable interface (PLIF)
- caller (consumer) interface
- provider interface
- plugin manager (PLM)
- plugin registry (PLR)
Pluggable interface
A pluggable interface is an interface, possibly internal to a library, that can be implemented by a third party in a modular, well-compartmentalized manner. These implementations of pluggable interfaces are called plugin modules. Pluggable interfaces allow a consumer to use the capabilities of the interface without needing to be aware of the implementation details. In particular, a pluggable interface prevents the consumer from needing to know whether the module is a built-in or a dynamically loadable module. Pluggable interfaces can be one-to-one, or one-to-many. An example of one-to-one is the DAL, and an example of one-to-many is preauth.
A pluggable interface has two parts: a consumer interface and a provider interface. Typically, library code implements the consumer interface, and application code or other library code calls the functions of the consumer interface. The provider interface is what the plugin module implements.
Caller (consumer) interface
The consumer interface isolates the consumer from implementation details of the pluggable interface. The caller does not generally need to know about whether a given module is built-in or dynamically loaded. The implementation of a consumer interface is essentially a glue layer, and can make use of domain-independent (not specific to any pluggable interface) capabilities of the plugin framework. The consumer might explicitly register a new module that it implements: this capability is part of a plugin registry.
A consumer of a pluggable interface uses an opaque handle, obtained from a loader function that is part of the pluggable interface, to call the methods of a plugin module. Each method of the consumer interface is an ordinary C function that takes the opaque handle either explicitly as its first argument or implicitly by some means such as a module name. Conceptually, these pluggable interface functions are wrapper functions that call through function pointers contained in the opaque plugin module handle object. (though this is not the only possible implementation)
A handle can represent:
- the plugin module itself
- a resource to which the plugin module provides access (e.g., a ccache handle)
- a set of plugin modules (e.g., the set of all available preauth mechanisms)
One rationale for using wrapper functions instead of having the caller directly invoke methods through a function pointer is to make it easier for debuggers and analysis tools to recognize when a particular interface method is being called. (Function pointers might have identifier names that look nothing like the actual name of the function they point to, in addition to enabling confusing aliasing.)
The loader function is specific to the pluggable interface. One reason is for type safety: there will be a distinct opaque handle type for each pluggable interface, allowing compile-time checking to catch some sorts of programming errors. Another reason is backward compatibility: it allows a pluggable interface to support plugin modules that implement an older provider interface.
Provider interface
A plugin module is a unit of code that implements the provider interface portion of a pluggable interface. Plugin modules can be built in or dynamically loaded. Several alternatives exist for the form of the provider interface, but some have significant advantages in allowing the plugin module to use identical source code for both built-in and loadable modules.
A built-in module is a module whose executable code is located within the library shared object or executable program file, or behaves as if it were. (While separate library shared objects that the calling library depends on can contain "built-in" modules for the calling library, this can cause problems with cyclic references.) The distinguishing characteristic of a built-in module is that, as part of program startup, the operating system automatically maps the executable code of the module into the address space of the process that calls it, without any explicit action by the library or program.
A dynamically loaded module is a module whose executable code is located within a file that is distinct from the library or program that calls it. The plugin support framework uses the runtime linker (or equivalent) to explicitly map the executable code of the module into the process address space. In POSIX systems, this is typically done using dlopen()
.
Loadable module provider interface
A loadable module exports a single function symbol, which denotes the vtable constructor function for that module. This function takes as arguments an interface version number, a pointer to a caller-allocated vtable structure for the interface, and the size of the structure (as an added precaution against version mismatches).
Although the caller (actually the plugin support code) allocates the vtable structure in the above description, one alternative is to have the module perform the allocation of the structure itself. This can cause problems if the module uses a different memory allocator than the caller.
Should the single function symbol have a prefix that depends on the name of the plugin module?
- Yes
- allows identical module source code to be used for the built-in and loadable module versions of a module
- No
- adding a prefix to the symbol adds complexity to the code that looks up the symbol
Built-in module provider interface
A built-in module provides the same interface as a loadable module. In the alternative where we use same exported function symbol for each loadable module implementing a pluggable interface, the built-in modules will still need distinct prefixes are required for each vtable retrieval function. This requires slightly different source code for a built-in module versus a loadable module.
Alternative: one symbol per method
Exporting one symbol per method in a loadable module is an alternative that might require less effort from a module author, as it does not require that a vtable be part of the provider interface. Built-in modules would probably still require a vtable (though that could be provided by the implementation of the consumer interface).
Alternative: loader with fixed module set
This approach uses a slightly different definition for "loader". A loader is a function that acts like a plugin registry that knows about a fixed, predetermined set of plugin modules. Each module in this set may implement a different pluggable interface. Typically, a built-in loader function knows about all of the built-in modules, regardless of what interface each module implements. A collection of modules (possibly implementing different pluggable interfaces) can be combined in a single dynamically loaded shared object that only exports the loader function interface. To search for a dynamically loaded plugin module, the plugin framework would load each of these shared objects and call the loader function of each one until it located the desired module.
Plugin manager
The plugin manager (PLM) provides a set of generic support capabilities that are independent of individual pluggable interfaces. It centralizes the discovery process for plugin modules. Typically, consumers of pluggable interfaces do not call it directly, but instead call a loader function of the pluggable interface, which in turn calls the plugin manager. The plugin manager works in concert with a set of plugin registries to locate plugin modules.
The krb5_init_context()
functions will create a plugin manager context that will exist in the krb5_context, and add the initial plugin registries to the plugin manager context. An application may use krb5_get_plm()
to retrieve the plugin manager context so it can register its own built-in plugin modules.
The plugin manager locates plugin modules using both a numeric identifer that designates a pluggable interface and a string that names a module that implements that pluggable interface. The primary way to use the plugin manager is to query it for the vtable constructor for a specified module (or a set of vtable constructors for all modules of that interface).
As a lower-level interface that supports backward compatibility for older loadable-module provider interfaces, the plugin manager can create an opaque locator handle for a specified module. The plugin manager also supports looking up sets of locator handles (for all modules implementing a specified pluggable interface).
Additional functions in the plugin manager interface allow for lookups of symbols in the plugin module (if supported by that module)
Plugin registry
A plugin registry (PLR) uses pairs of plugin interface identifiers and plugin module names to look up modules. Most of the functionality of a plugin registry is only accessible indirectly through the plugin manager, but some user-level functionality exists for each type of registry, such as an interface to create a new registry or to register a new plugin module in a registry. The primary way to use a registry is to query it for the vtable constructor for a specified module.
A plugin registry can also produce a opaque module locator handle for a specified module. It provides lookup functions so that a loader function of a pluggable interface can do the equivalent of dlsym()
on the module represented by a locator handle.
Initially, the plugin framework implementation will contain two types of registry:
- built-in module table registry
- locates built-in plugin modules. This also allows an application to register its private built-in plugin modules.
- loadable module directories registry
- locates dynamically loadable plugin modules. It maps plugin interface identifiers to directories where modules implementing that interface can be found, and can thereby locate individual modules.
The krb5_init_context()
functions will call various registry methods to create and configure registries for libkrb5 built-in modules and default directories for loadable modules.
Registry of built-in modules
This kind of registry keeps track of built-in modules. Typically, libkrb5 will initialize this with locators for all of the built-in modules that are linked into it. Applications can also register private built-in plugin modules using this type of registry. TO BE DETERMINED: Are libkrb5 registry tables read-only? If they are, applications will probably need to add their own registries to the plugin manager in order to register their private built-in plugin modules.
Registry of loadable modules
This kind of registry keeps track of a few additional items needed for locating loadable modules. This includes a base directory pathname to directory tree of plugin modules. Each subdirectory of the base directory has a name that corresponds to the pluggable interface identifier for the modules that intended to be in that subdirectory.
This registry type has functions to configure the mapping of pluggable interface identifiers to subdirectory names, as well as functions to configure the mapping of individual loadable module files to module names / interface identifier pairs. The krb5_init_context()
functions will do this for the pluggable interfaces that are part of libkrb5.
NEEDS MORE DETAIL
Details
Deallocator functions, etc. omitted for conciseness.
NEEDS MORE DETAIL
Plugin manager details
The following functions are meant to be used by a consumer of pluggable interfaces:
-
k5_plm_init
- initialize a plugin manager context
-
k5_plm_fini
- shut down a plugin manager context
-
k5_plm_add_reg
- add a new registry to a plugin manager context
The following functions are meant to be used by a loader function of a pluggable interface:
-
k5_plm_get_vtfn
- get a vtable constructor
-
k5_plm_get_vtfn_set
- get a set of vtable constructors
The following functions are meant to be used by a loader function that needs to do lower-level operations (particularly for backward compatibility with older loadable modules). They are typically not implemented by registries that only track built-in modules:
-
k5_plm_get_ploc
- retrieve a module locator handle
-
k5_plm_get_ploc_set
- retrieve a set of module locator handles
-
k5_ploc_get_vtfn
- get a vtable constructor function from a module locator
-
k5_ploc_get_data
- get a data symbol from a module locator
-
k5_ploc_get_func
- get a function symbol from a module locator
-
k5_ploc_get_vtfn_set
- get a set of vtable constructor functions from a module locator set
-
k5_ploc_get_data_set
- get a set of data symbol from a module locator set
-
k5_ploc_get_func_set
- get a set of function symbol from a module locator set
Old background
for reference
Current plugins
We currently have the following plugin frameworks:
- Preauth: All shared objects from profile-specified or installation directory are loaded. Two vtables are read from the shared objects, one for libkrb5 and one for the KDC. The preauth framework iterates over the module list invoking functions to generate or handle preauth data. Preauth vtable functions receive a callback function and data object which allow it to request information such as the expected enctype or FAST armor key for the request.
- Authdata: Very similar to the preauth framework.
- KDB: The profile specifies a database library name for each realm. Shared objects matching the library name are loaded from a profile-specified and installation directory; the first matching object with an appropriately-named vtable data object is used, and the rest are ignored. libkdb5 contains wrappers which invoke functions in the library's vtable, or (for some optional functions) default implementations if the vtable left the function pointer as NULL.
- KDC location: All shared objects from an installation directory are located. A vtable is read from the shared objects. The KDC location framework iterates over each vtable and invokes a lookup function; modules can return success with a location, an error (which halts the location process), or a distinguished error code which passes control along to the next module or the built-in location mechanisms.
- GSSAPI: The file /etc/gss/mechs can specify a list of mechanism OIDs and shared object filenames; filenames are taken as relative to an installation directory. Shared objects implementing mechanisms can export either a function returning a vtable, or can export each GSSAPI interface individually.
The following areas of functionality are virtualized but have no exposed plugin framework:
- Serialization: Serialization table entries can be registered with krb5_register_serializer. Data objects are matched to table entries by magic number. The registration function is exported by libkrb5 and is named with the krb5_ prefix, but it and its associated structure are declared in k5-int.h rather than krb5.h. It is not used outside of libkrb5.
- ccache: Very similar to serialization, except that ccache implementations are selected using a URL-style prefix in the ccache name.
- keytab: Very similar to ccache, except that the keytab registration function is used outside of libkrb5 to register a "KDB keytab", which is used by kadmind to serve GSSRPC without requiring a keytab file containing the kadmin keys.
- Replay cache: Very similar to ccache, except that the replay cache registration function is not used anywhere (even inside libkrb5).
Plugin frameworks which are "not exposed" may still be productively used by vendor forks of the krb5 tree.
Future plugins
The following areas are candidates for future plugin support:
- PRNG
- profile / configuration
- DNS / host-realm mapping
- password quality policy
- lockout
- audit
- password synchronization
Current support infrastructure
In libkrb5support, we have functions to facilitate loading plugins from shared objects. There is a set of functions to load individual plugins from named files and mechglue; these are currently used by the HDB bridge and GSS mechglue:
- krb5int_open_plugin - Create a plugin handle from a filename
- krb5int_close_plugin - Close a plugin handle
- krb5int_get_plugin_data - Retrieve a data object from a plugin handle by symbol name
- krb5int_get_plugin_func - Retrieve a function object from a plugin handle by symbol name
There is another set of functions to scan a list of directories for plugins:
- krb5int_open_plugin_dirs - Create a plugin dir handle from a list of directories and (optionally) filebases
- krb5int_close_plugin_dirs - Close a plugin dir handle
- krb5int_get_plugin_dir_data - Retrieve a list of data objects from a plugin dir handle by symbol name
- krb5int_get_plugin_dir_func - Retrieve a list of function objects from a plugin dir handle by symbol name
- krb5int_free_plugin_dir_data - Free a list of data objects returned by krb5int_get_plugin_dir_data
- krb5int_free_plugin_dir_func - Free a list of function objects returned by krb5int_get_plugin_dir_func
Problem areas
- Every caller of krb5int_open_plugin_dirs specifies either no filebases (e.g. preauth plugins) or a single filebase (KDB plugins). Accepting and processing a list of filebases is probably needless complexity.
- Callers of krb5int_open_plugin_dirs have to know what directories to supply, which means they need to know the krb5 install root as well as the magic plugin area for OS X, and they need logic for reading a profile variable to determine the alternate plugin directory for the test suite (currently only implemented for KDB and preauth plugins).
- In most uses of plugins, we read a data object containing a list of function pointers. This makes it mostly impossible to supply a plugin which works with multiple versions of krb5. If we instead read a function object which we invoked with a version number to retrieve the vtable, it would be possible (though perhaps awkward) to create a shared object which works with multiple versions.
- We are somewhat schizophrenic about how plugins can access krb5 library functionality, and in particular internal symbols. Sometimes we call functions directly, sometimes we make use of a vtable passed into the plugin (e.g. the preauth_get_client_data_proc function), sometimes we use the accessor to invoke internal functions, and sometimes we call APIs or internal functions directly. Ideally we should have a consistent policy with a sound justification.
- When measuring code coverage with gcov, we cannot use shared libraries; this means we need to link in-tree plugins statically into the libraries or programs which load them. We have an ad-hoc method to do this with KDB plugins, but not with other plugin types.
- Administrators have an easier time writing scripts than creating linkable shared objects. In some cases it might yield a better administrator experience to create plugin interfaces via subprocesses than loading shared objects, although in many cases this might not be feasible.
- In some scenarios such as embedded environments, it may be more useful to allow applications to supply plugin vtables via an API (as we do for keytabs and ccaches, though those APIs are not public) than to load them from shared objects in the filesystem.
Definitions
- pluggable interface
- an (internal) interface that can be implemented by a third party. These can be one-to-one, or one-to-many. An example of one-to-one is the DAL, and an example of one-to-many is preauth.
- module
- a unit of code that implements a pluggable interface. It can be built in, or it can be dynamically loadable.
- built-in
- a module whose executable code is located within the library shared object or executable program file, or behaves as if it were. (While separate library shared objects that the calling library depends on can contain "built-in" modules for the calling library, this can cause problems with cyclic references.) The distinguishing characteristic of a built-in module is that, as part of program startup, the operating system automatically maps the executable code of the module into the address space of the process that calls it, without any explicit action by the library or program.
- dynamically loaded
- a module whose executable code is located within a file that is distinct from the library or program that calls it. The plugin support framework uses the runtime linker (or equivalent) to explicitly map the executable code of the module into the process address space. In POSIX systems, this is typically done using
dlopen()
.
- discovery
- process of enumerating what modules are available for a pluggable interface. Includes possible filtering of the raw discovered set.
- compiled-in
- directory scan
- explicit inclusion by configuration
- explicit exclusion by configuration
- loading
- the process of making modules available for calling. This can involve dynamically loading a module using the runtime linker, or it can involve registering a vtable provided by an application.
- built-in
- dynamic loading
- application-registered
- selection
- the process of a caller invoking one specific module from the set of loaded modules that implement an interface.
- consumer interface
- the interface that a caller uses to access the services of a pluggable interface. Typically, but not always, the krb5 library implements the consumer interface.
- provider interface
- the interface that a module author implements
Links
Some possibly useful ideas at Dr Dobb's. It's rather biased toward C++, and we may disagree with some of the details. Among other things, it advocates separating the domain-specific interface of a plugin module from its domain-independent interface: plugin modules get passed a handle for the "registry services" of the plugin manager, and the modules must call those services to register their objects or methods.