1/*	$NetBSD: plugin.c,v 1.3 2023/06/19 21:41:44 christos Exp $	*/
2
3/*
4 * Copyright (c) 2006 - 2007 Kungliga Tekniska H��gskolan
5 * (Royal Institute of Technology, Stockholm, Sweden).
6 * All rights reserved.
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
10 * are met:
11 *
12 * 1. Redistributions of source code must retain the above copyright
13 *    notice, this list of conditions and the following disclaimer.
14 *
15 * 2. Redistributions in binary form must reproduce the above copyright
16 *    notice, this list of conditions and the following disclaimer in the
17 *    documentation and/or other materials provided with the distribution.
18 *
19 * 3. Neither the name of the Institute nor the names of its contributors
20 *    may be used to endorse or promote products derived from this software
21 *    without specific prior written permission.
22 *
23 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
24 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26 * ARE DISCLAIMED.  IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
27 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
29 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
30 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
32 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
33 * SUCH DAMAGE.
34 */
35
36#include "krb5_locl.h"
37
38#ifdef HAVE_DLFCN_H
39#include <dlfcn.h>
40#endif
41#include <dirent.h>
42
43struct krb5_plugin {
44    void *symbol;
45    struct krb5_plugin *next;
46};
47
48struct plugin {
49    enum { DSO, SYMBOL } type;
50    union {
51	struct {
52	    char *path;
53	    void *dsohandle;
54	} dso;
55	struct {
56	    enum krb5_plugin_type type;
57	    char *name;
58	    char *symbol;
59	} symbol;
60    } u;
61    struct plugin *next;
62};
63
64static HEIMDAL_MUTEX plugin_mutex = HEIMDAL_MUTEX_INITIALIZER;
65static struct plugin *registered = NULL;
66
67/**
68 * Register a plugin symbol name of specific type.
69 * @param context a Keberos context
70 * @param type type of plugin symbol
71 * @param name name of plugin symbol
72 * @param symbol a pointer to the named symbol
73 * @return In case of error a non zero error com_err error is returned
74 * and the Kerberos error string is set.
75 *
76 * @ingroup krb5_support
77 */
78
79KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
80krb5_plugin_register(krb5_context context,
81		     enum krb5_plugin_type type,
82		     const char *name,
83		     void *symbol)
84{
85    struct plugin *e;
86
87    HEIMDAL_MUTEX_lock(&plugin_mutex);
88
89    /* check for duplicates */
90    for (e = registered; e != NULL; e = e->next) {
91	if (e->type == SYMBOL &&
92	    strcmp(e->u.symbol.name, name) == 0 &&
93	    e->u.symbol.type == type && e->u.symbol.symbol == symbol) {
94	    HEIMDAL_MUTEX_unlock(&plugin_mutex);
95	    return 0;
96	}
97    }
98
99    e = calloc(1, sizeof(*e));
100    if (e == NULL) {
101	HEIMDAL_MUTEX_unlock(&plugin_mutex);
102	krb5_set_error_message(context, ENOMEM, "malloc: out of memory");
103	return ENOMEM;
104    }
105    e->type = SYMBOL;
106    e->u.symbol.type = type;
107    e->u.symbol.name = strdup(name);
108    if (e->u.symbol.name == NULL) {
109	HEIMDAL_MUTEX_unlock(&plugin_mutex);
110	free(e);
111	krb5_set_error_message(context, ENOMEM, "malloc: out of memory");
112	return ENOMEM;
113    }
114    e->u.symbol.symbol = symbol;
115
116    e->next = registered;
117    registered = e;
118    HEIMDAL_MUTEX_unlock(&plugin_mutex);
119
120    return 0;
121}
122
123static krb5_error_code
124add_symbol(krb5_context context, struct krb5_plugin **list, void *symbol)
125{
126    struct krb5_plugin *e;
127
128    e = calloc(1, sizeof(*e));
129    if (e == NULL) {
130	krb5_set_error_message(context, ENOMEM, "malloc: out of memory");
131	return ENOMEM;
132    }
133    e->symbol = symbol;
134    e->next = *list;
135    *list = e;
136    return 0;
137}
138
139KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
140_krb5_plugin_find(krb5_context context,
141		  enum krb5_plugin_type type,
142		  const char *name,
143		  struct krb5_plugin **list)
144{
145    struct plugin *e;
146    krb5_error_code ret;
147
148    *list = NULL;
149
150    HEIMDAL_MUTEX_lock(&plugin_mutex);
151
152    for (ret = 0, e = registered; e != NULL; e = e->next) {
153	switch(e->type) {
154	case DSO: {
155	    void *sym;
156	    if (e->u.dso.dsohandle == NULL)
157		continue;
158	    sym = dlsym(e->u.dso.dsohandle, name);
159	    if (sym)
160		ret = add_symbol(context, list, sym);
161	    break;
162	}
163	case SYMBOL:
164	    if (strcmp(e->u.symbol.name, name) == 0 && e->u.symbol.type == type)
165		ret = add_symbol(context, list, e->u.symbol.symbol);
166	    break;
167	}
168	if (ret) {
169	    _krb5_plugin_free(*list);
170	    *list = NULL;
171	}
172    }
173
174    HEIMDAL_MUTEX_unlock(&plugin_mutex);
175    if (ret)
176	return ret;
177
178    if (*list == NULL) {
179	krb5_set_error_message(context, ENOENT, "Did not find a plugin for %s", name);
180	return ENOENT;
181    }
182
183    return 0;
184}
185
186KRB5_LIB_FUNCTION void KRB5_LIB_CALL
187_krb5_plugin_free(struct krb5_plugin *list)
188{
189    struct krb5_plugin *next;
190    while (list) {
191	next = list->next;
192	free(list);
193	list = next;
194    }
195}
196/*
197 * module - dict of {
198 *      ModuleName = [
199 *          plugin = object{
200 *              array = { ptr, ctx }
201 *          }
202 *      ]
203 * }
204 */
205
206static heim_dict_t modules;
207
208struct plugin2 {
209    heim_string_t path;
210    void *dsohandle;
211    heim_dict_t names;
212};
213
214static void
215plug_dealloc(void *ptr)
216{
217    struct plugin2 *p = ptr;
218    heim_release(p->path);
219    heim_release(p->names);
220    if (p->dsohandle)
221	dlclose(p->dsohandle);
222}
223
224static char *
225resolve_origin(const char *di)
226{
227#ifdef HAVE_DLADDR
228    Dl_info dl_info;
229    const char *dname;
230    char *path, *p;
231#endif
232
233    if (strncmp(di, "$ORIGIN/", sizeof("$ORIGIN/") - 1) &&
234        strcmp(di, "$ORIGIN"))
235        return strdup(di);
236
237#ifndef HAVE_DLADDR
238    return strdup(LIBDIR "/plugin/krb5");
239#else /* !HAVE_DLADDR */
240    di += sizeof("$ORIGIN") - 1;
241
242    if (dladdr(_krb5_load_plugins, &dl_info) == 0)
243        return strdup(LIBDIR "/plugin/krb5");
244
245    dname = dl_info.dli_fname;
246#ifdef _WIN32
247    p = strrchr(dname, '\\');
248    if (p == NULL)
249#endif
250	p = strrchr(dname, '/');
251    if (p) {
252        if (asprintf(&path, "%.*s%s", (int) (p - dname), dname, di) == -1)
253            return NULL;
254    } else {
255        if (asprintf(&path, "%s%s", dname, di) == -1)
256            return NULL;
257    }
258
259    return path;
260#endif /* !HAVE_DLADDR */
261}
262
263
264/**
265 * Load plugins (new system) for the given module @name (typically
266 * "krb5") from the given directory @paths.
267 *
268 * Inputs:
269 *
270 * @context A krb5_context
271 * @name    Name of plugin module (typically "krb5")
272 * @paths   Array of directory paths where to look
273 */
274KRB5_LIB_FUNCTION void KRB5_LIB_CALL
275_krb5_load_plugins(krb5_context context, const char *name, const char **paths)
276{
277#ifdef HAVE_DLOPEN
278    heim_string_t s = heim_string_create(name);
279    heim_dict_t module;
280    struct dirent *entry;
281    krb5_error_code ret;
282    const char **di;
283    char *dirname = NULL;
284    DIR *d;
285#ifdef _WIN32
286    const char * plugin_prefix;
287    size_t plugin_prefix_len;
288
289    if (asprintf(&plugin_prefix, "plugin_%s_", name) == -1)
290	return;
291    plugin_prefix_len = (plugin_prefix ? strlen(plugin_prefix) : 0);
292#endif
293
294    HEIMDAL_MUTEX_lock(&plugin_mutex);
295
296    if (modules == NULL) {
297	modules = heim_dict_create(11);
298	if (modules == NULL) {
299	    HEIMDAL_MUTEX_unlock(&plugin_mutex);
300	    return;
301	}
302    }
303
304    module = heim_dict_copy_value(modules, s);
305    if (module == NULL) {
306	module = heim_dict_create(11);
307	if (module == NULL) {
308	    HEIMDAL_MUTEX_unlock(&plugin_mutex);
309	    heim_release(s);
310	    return;
311	}
312	heim_dict_set_value(modules, s, module);
313    }
314    heim_release(s);
315
316    for (di = paths; *di != NULL; di++) {
317        free(dirname);
318        dirname = resolve_origin(*di);
319        if (dirname == NULL)
320            continue;
321	d = opendir(dirname);
322	if (d == NULL)
323	    continue;
324	rk_cloexec_dir(d);
325
326	while ((entry = readdir(d)) != NULL) {
327	    char *n = entry->d_name;
328	    char *path = NULL;
329	    heim_string_t spath;
330	    struct plugin2 *p;
331
332	    /* skip . and .. */
333	    if (n[0] == '.' && (n[1] == '\0' || (n[1] == '.' && n[2] == '\0')))
334		continue;
335
336	    ret = 0;
337#ifdef _WIN32
338	    /*
339	     * On Windows, plugins must be loaded from the same directory as
340	     * heimdal.dll (typically the assembly directory) and must have
341	     * the name form "plugin_<module>_<name>.dll".
342	     */
343	    {
344		char *ext;
345
346		if (strnicmp(n, plugin_prefix, plugin_prefix_len))
347		    continue;
348		ext = strrchr(n, '.');
349		if (ext == NULL || stricmp(ext, ".dll"))
350		     continue;
351
352		ret = asprintf(&path, "%s\\%s", dirname, n);
353		if (ret < 0 || path == NULL)
354		    continue;
355	    }
356#endif
357#ifdef __APPLE__
358	    { /* support loading bundles on MacOS */
359		size_t len = strlen(n);
360		if (len > 7 && strcmp(&n[len - 7],  ".bundle") == 0)
361		    ret = asprintf(&path, "%s/%s/Contents/MacOS/%.*s", dirname, n, (int)(len - 7), n);
362	    }
363#endif
364	    if (ret < 0 || path == NULL)
365		ret = asprintf(&path, "%s/%s", dirname, n);
366
367	    if (ret < 0 || path == NULL)
368		continue;
369
370	    spath = heim_string_create(n);
371	    if (spath == NULL) {
372		free(path);
373		continue;
374	    }
375
376	    /* check if already cached */
377	    p = heim_dict_copy_value(module, spath);
378	    if (p == NULL) {
379		p = heim_alloc(sizeof(*p), "krb5-plugin", plug_dealloc);
380		if (p)
381		    p->dsohandle = dlopen(path, RTLD_LOCAL|RTLD_LAZY);
382
383		if (p && p->dsohandle) {
384		    p->path = heim_retain(spath);
385		    p->names = heim_dict_create(11);
386		    heim_dict_set_value(module, spath, p);
387		}
388	    }
389            heim_release(p);
390	    heim_release(spath);
391	    free(path);
392	}
393	closedir(d);
394    }
395    free(dirname);
396    HEIMDAL_MUTEX_unlock(&plugin_mutex);
397    heim_release(module);
398#ifdef _WIN32
399    if (plugin_prefix)
400	free(plugin_prefix);
401#endif
402#endif /* HAVE_DLOPEN */
403}
404
405/**
406 * Unload plugins (new system)
407 */
408KRB5_LIB_FUNCTION void KRB5_LIB_CALL
409_krb5_unload_plugins(krb5_context context, const char *name)
410{
411    HEIMDAL_MUTEX_lock(&plugin_mutex);
412    heim_release(modules);
413    modules = NULL;
414    HEIMDAL_MUTEX_unlock(&plugin_mutex);
415}
416
417/*
418 *
419 */
420
421struct common_plugin_method {
422    int			version;
423    krb5_error_code	(*init)(krb5_context, void **);
424    void		(*fini)(void *);
425};
426
427struct plug {
428    void *dataptr;
429    void *ctx;
430};
431
432static void
433plug_free(void *ptr)
434{
435    struct plug *pl = ptr;
436    if (pl->dataptr) {
437	struct common_plugin_method *cpm = pl->dataptr;
438	cpm->fini(pl->ctx);
439    }
440}
441
442struct iter_ctx {
443    krb5_context context;
444    heim_string_t n;
445    const char *name;
446    int min_version;
447    int flags;
448    heim_array_t result;
449    krb5_error_code (KRB5_LIB_CALL *func)(krb5_context, const void *, void *, void *);
450    void *userctx;
451    krb5_error_code ret;
452};
453
454static void
455search_modules(heim_object_t key, heim_object_t value, void *ctx)
456{
457    struct iter_ctx *s = ctx;
458    struct plugin2 *p = value;
459    struct plug *pl = heim_dict_copy_value(p->names, s->n);
460    struct common_plugin_method *cpm;
461
462    if (pl == NULL) {
463	if (p->dsohandle == NULL)
464	    return;
465
466	pl = heim_alloc(sizeof(*pl), "struct-plug", plug_free);
467
468	cpm = pl->dataptr = dlsym(p->dsohandle, s->name);
469	if (cpm) {
470	    int ret;
471
472	    ret = cpm->init(s->context, &pl->ctx);
473	    if (ret)
474		cpm = pl->dataptr = NULL;
475	}
476	heim_dict_set_value(p->names, s->n, pl);
477    } else {
478	cpm = pl->dataptr;
479    }
480
481    if (cpm && cpm->version >= s->min_version)
482	heim_array_append_value(s->result, pl);
483    heim_release(pl);
484}
485
486static void
487eval_results(heim_object_t value, void *ctx, int *stop)
488{
489    struct plug *pl = value;
490    struct iter_ctx *s = ctx;
491
492    if (s->ret != KRB5_PLUGIN_NO_HANDLE)
493	return;
494
495    s->ret = s->func(s->context, pl->dataptr, pl->ctx, s->userctx);
496    if (s->ret != KRB5_PLUGIN_NO_HANDLE
497        && !(s->flags & KRB5_PLUGIN_INVOKE_ALL))
498        *stop = 1;
499}
500
501/**
502 * Run plugins for the given @module (e.g., "krb5") and @name (e.g.,
503 * "kuserok").  Specifically, the @func is invoked once per-plugin with
504 * four arguments: the @context, the plugin symbol value (a pointer to a
505 * struct whose first three fields are the same as struct common_plugin_method),
506 * a context value produced by the plugin's init method, and @userctx.
507 *
508 * @func should unpack arguments for a plugin function and invoke it
509 * with arguments taken from @userctx.  @func should save plugin
510 * outputs, if any, in @userctx.
511 *
512 * All loaded and registered plugins are invoked via @func until @func
513 * returns something other than KRB5_PLUGIN_NO_HANDLE.  Plugins that
514 * have nothing to do for the given arguments should return
515 * KRB5_PLUGIN_NO_HANDLE.
516 *
517 * Inputs:
518 *
519 * @context     A krb5_context
520 * @module      Name of module (typically "krb5")
521 * @name        Name of pluggable interface (e.g., "kuserok")
522 * @min_version Lowest acceptable plugin minor version number
523 * @flags       Flags (none defined at this time)
524 * @userctx     Callback data for the callback function @func
525 * @func        A callback function, invoked once per-plugin
526 *
527 * Outputs: None, other than the return value and such outputs as are
528 *          gathered by @func.
529 */
530KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL
531_krb5_plugin_run_f(krb5_context context,
532		   const char *module,
533		   const char *name,
534		   int min_version,
535		   int flags,
536		   void *userctx,
537		   krb5_error_code (KRB5_LIB_CALL *func)(krb5_context, const void *, void *, void *))
538{
539    heim_string_t m = heim_string_create(module);
540    heim_dict_t dict;
541    void *plug_ctx;
542    struct common_plugin_method *cpm;
543    struct iter_ctx s;
544    struct krb5_plugin *registered_plugins = NULL;
545    struct krb5_plugin *p;
546
547    /* Get registered plugins */
548    (void) _krb5_plugin_find(context, PLUGIN_TYPE_DATA, name, &registered_plugins);
549
550    HEIMDAL_MUTEX_lock(&plugin_mutex);
551
552    s.context = context;
553    s.name = name;
554    s.n = heim_string_create(name);
555    s.flags = flags;
556    s.min_version = min_version;
557    s.result = heim_array_create();
558    s.func = func;
559    s.userctx = userctx;
560    s.ret = KRB5_PLUGIN_NO_HANDLE;
561
562    /* Get loaded plugins */
563    dict = heim_dict_copy_value(modules, m);
564    heim_release(m);
565
566    /* Add loaded plugins to s.result array */
567    if (dict)
568	heim_dict_iterate_f(dict, &s, search_modules);
569
570    /* We don't need to hold plugin_mutex during plugin invocation */
571    HEIMDAL_MUTEX_unlock(&plugin_mutex);
572
573    /* Invoke registered plugins (old system) */
574    for (p = registered_plugins; p; p = p->next) {
575	/*
576	 * XXX This is the wrong way to handle registered plugins, as we
577	 * call init/fini on each invocation!  We do this because we
578	 * have nowhere in the struct plugin registered list to store
579	 * the context allocated by the plugin's init function.  (But at
580	 * least we do call init/fini!)
581	 *
582	 * What we should do is adapt the old plugin system to the new
583	 * one and change how we register plugins so that we use the new
584	 * struct plug to keep track of their context structures, that
585	 * way we can init once, invoke many times, then fini.
586	 */
587	cpm = (struct common_plugin_method *)p->symbol;
588	s.ret = cpm->init(context, &plug_ctx);
589	if (s.ret)
590	    continue;
591	s.ret = s.func(s.context, p->symbol, plug_ctx, s.userctx);
592	cpm->fini(plug_ctx);
593	if (s.ret != KRB5_PLUGIN_NO_HANDLE &&
594            !(flags & KRB5_PLUGIN_INVOKE_ALL))
595	    break;
596    }
597    _krb5_plugin_free(registered_plugins);
598
599    /* Invoke loaded plugins (new system) */
600    if (s.ret == KRB5_PLUGIN_NO_HANDLE)
601	heim_array_iterate_f(s.result, &s, eval_results);
602
603    heim_release(s.result);
604    heim_release(s.n);
605    heim_release(dict);
606
607    return s.ret;
608}
609