1 /* Copyright (c) 2000, 2021, Oracle and/or its affiliates.
2
3 This program is free software; you can redistribute it and/or modify
4 it under the terms of the GNU General Public License, version 2.0,
5 as published by the Free Software Foundation.
6
7 This program is also distributed with certain software (including
8 but not limited to OpenSSL) that is licensed under separate terms,
9 as designated in a particular file or component or in included license
10 documentation. The authors of MySQL hereby grant you an additional
11 permission to link the program and your derivative works with the
12 separately licensed software that they have included with MySQL.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License, version 2.0, for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software Foundation,
21 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA */
22
23 /* This implements 'user defined functions' */
24
25 /*
26 Known bugs:
27
28 Memory for functions is never freed!
29 Shared libraries are not closed before mysqld exits;
30 - This is because we can't be sure if some threads are using
31 a function.
32
33 The bugs only affect applications that create and free a lot of
34 dynamic functions, so this shouldn't be a real problem.
35 */
36
37 #include "sql_base.h" // close_mysql_tables
38 #include "sql_parse.h" // check_string_char_length
39 #include "sql_table.h" // write_bin_log
40 #include "records.h" // init_read_record, end_read_record
41 #include "lock.h" // MYSQL_LOCK_IGNORE_TIMEOUT
42 #include "log.h"
43 #include "sql_plugin.h" // check_valid_path
44
45 #ifdef HAVE_DLFCN_H
46 #include <dlfcn.h>
47 #endif
48
49 #ifdef HAVE_DLOPEN
50 #include <stdarg.h>
51 #include <hash.h>
52
53 static bool initialized = 0;
54 static MEM_ROOT mem;
55 static HASH udf_hash;
56 static mysql_rwlock_t THR_LOCK_udf;
57
58
59 static udf_func *add_udf(LEX_STRING *name, Item_result ret,
60 char *dl, Item_udftype typ);
61 static void del_udf(udf_func *udf);
62 static void *find_udf_dl(const char *dl);
63
init_syms(udf_func * tmp,char * nm)64 static char *init_syms(udf_func *tmp, char *nm)
65 {
66 char *end;
67
68 if (!((tmp->func= (Udf_func_any) dlsym(tmp->dlhandle, tmp->name.str))))
69 return tmp->name.str;
70
71 end=my_stpcpy(nm,tmp->name.str);
72
73 if (tmp->type == UDFTYPE_AGGREGATE)
74 {
75 (void)my_stpcpy(end, "_clear");
76 if (!((tmp->func_clear= (Udf_func_clear) dlsym(tmp->dlhandle, nm))))
77 return nm;
78 (void)my_stpcpy(end, "_add");
79 if (!((tmp->func_add= (Udf_func_add) dlsym(tmp->dlhandle, nm))))
80 return nm;
81 }
82
83 (void) my_stpcpy(end,"_deinit");
84 tmp->func_deinit= (Udf_func_deinit) dlsym(tmp->dlhandle, nm);
85
86 (void) my_stpcpy(end,"_init");
87 tmp->func_init= (Udf_func_init) dlsym(tmp->dlhandle, nm);
88
89 /*
90 to prefent loading "udf" from, e.g. libc.so
91 let's ensure that at least one auxiliary symbol is defined
92 */
93 if (!tmp->func_init && !tmp->func_deinit && tmp->type != UDFTYPE_AGGREGATE)
94 {
95 if (!opt_allow_suspicious_udfs)
96 return nm;
97 sql_print_warning(ER(ER_CANT_FIND_DL_ENTRY), nm);
98 }
99 return 0;
100 }
101
102
get_hash_key(const uchar * buff,size_t * length,my_bool not_used MY_ATTRIBUTE ((unused)))103 extern "C" uchar* get_hash_key(const uchar *buff, size_t *length,
104 my_bool not_used MY_ATTRIBUTE((unused)))
105 {
106 udf_func *udf=(udf_func*) buff;
107 *length=(uint) udf->name.length;
108 return (uchar*) udf->name.str;
109 }
110
111 static PSI_memory_key key_memory_udf_mem;
112
113 #ifdef HAVE_PSI_INTERFACE
114 static PSI_rwlock_key key_rwlock_THR_LOCK_udf;
115
116 static PSI_rwlock_info all_udf_rwlocks[]=
117 {
118 { &key_rwlock_THR_LOCK_udf, "THR_LOCK_udf", PSI_FLAG_GLOBAL}
119 };
120
121 static PSI_memory_info all_udf_memory[]=
122 {
123 { &key_memory_udf_mem, "udf_mem", PSI_FLAG_GLOBAL}
124 };
125
init_udf_psi_keys(void)126 static void init_udf_psi_keys(void)
127 {
128 const char* category= "sql";
129 int count;
130
131 count= array_elements(all_udf_rwlocks);
132 mysql_rwlock_register(category, all_udf_rwlocks, count);
133
134 count= array_elements(all_udf_memory);
135 mysql_memory_register(category, all_udf_memory, count);
136 }
137 #endif
138
139 /*
140 Read all predeclared functions from mysql.func and accept all that
141 can be used.
142 */
143
udf_init()144 void udf_init()
145 {
146 udf_func *tmp;
147 TABLE_LIST tables;
148 READ_RECORD read_record_info;
149 TABLE *table;
150 int error;
151 DBUG_ENTER("ufd_init");
152 char db[]= "mysql"; /* A subject to casednstr, can't be constant */
153
154 if (initialized)
155 DBUG_VOID_RETURN;
156
157 #ifdef HAVE_PSI_INTERFACE
158 init_udf_psi_keys();
159 #endif
160
161 mysql_rwlock_init(key_rwlock_THR_LOCK_udf, &THR_LOCK_udf);
162
163 init_sql_alloc(key_memory_udf_mem, &mem, UDF_ALLOC_BLOCK_SIZE, 0);
164 THD *new_thd = new THD;
165 if (!new_thd ||
166 my_hash_init(&udf_hash,system_charset_info,32,0,0,get_hash_key, NULL, 0,
167 key_memory_udf_mem))
168 {
169 sql_print_error("Can't allocate memory for udf structures");
170 my_hash_free(&udf_hash);
171 free_root(&mem,MYF(0));
172 delete new_thd;
173 DBUG_VOID_RETURN;
174 }
175 initialized = 1;
176 new_thd->thread_stack= (char*) &new_thd;
177 new_thd->store_globals();
178 {
179 LEX_CSTRING db_lex_cstr= { STRING_WITH_LEN(db) };
180 new_thd->set_db(db_lex_cstr);
181 }
182
183 tables.init_one_table(db, sizeof(db)-1, "func", 4, "func", TL_READ);
184
185 if (open_and_lock_tables(new_thd, &tables, MYSQL_LOCK_IGNORE_TIMEOUT))
186 {
187 DBUG_PRINT("error",("Can't open udf table"));
188 sql_print_error("Can't open the mysql.func table. Please "
189 "run mysql_upgrade to create it.");
190 goto end;
191 }
192
193 table= tables.table;
194 if (init_read_record(&read_record_info, new_thd, table, NULL, 1, 1, FALSE))
195 goto end;
196 table->use_all_columns();
197 while (!(error= read_record_info.read_record(&read_record_info)))
198 {
199 DBUG_PRINT("info",("init udf record"));
200 LEX_STRING name;
201 name.str=get_field(&mem, table->field[0]);
202
203 // Check the name.str is NULL or not.
204 if (name.str == NULL)
205 {
206 sql_print_error("Invalid row in mysql.func table for column 'name'");
207 continue;
208 }
209
210 name.length = strlen(name.str);
211 char *dl_name= get_field(&mem, table->field[2]);
212 if (dl_name == NULL)
213 {
214 sql_print_error("Invalid row in mysql.func table for function '%.64s'",
215 name.str);
216 continue;
217 }
218 bool new_dl=0;
219 Item_udftype udftype=UDFTYPE_FUNCTION;
220 if (table->s->fields >= 4) // New func table
221 udftype=(Item_udftype) table->field[3]->val_int();
222
223 /*
224 Ensure that the .dll doesn't have a path
225 This is done to ensure that only approved dll from the system
226 directories are used (to make this even remotely secure).
227
228 On windows we must check both FN_LIBCHAR and '/'.
229 */
230
231 LEX_CSTRING name_cstr= {name.str, name.length};
232 if (check_valid_path(dl_name, strlen(dl_name)) ||
233 check_string_char_length(name_cstr, "", NAME_CHAR_LEN,
234 system_charset_info, 1))
235 {
236 sql_print_error("Invalid row in mysql.func table for function '%.64s'",
237 name.str);
238 continue;
239 }
240
241 if (!(tmp= add_udf(&name,(Item_result) table->field[1]->val_int(),
242 dl_name, udftype)))
243 {
244 sql_print_error("Can't alloc memory for udf function: '%.64s'", name.str);
245 continue;
246 }
247
248 void *dl = find_udf_dl(tmp->dl);
249 if (dl == NULL)
250 {
251 char dlpath[FN_REFLEN];
252 strxnmov(dlpath, sizeof(dlpath) - 1, opt_plugin_dir, "/", tmp->dl,
253 NullS);
254 (void) unpack_filename(dlpath, dlpath);
255 if (!(dl= dlopen(dlpath, RTLD_NOW)))
256 {
257 const char *errmsg;
258 int error_number= dlopen_errno;
259 DLERROR_GENERATE(errmsg, error_number);
260
261 /* Print warning to log */
262 sql_print_error(ER(ER_CANT_OPEN_LIBRARY), tmp->dl, error_number, errmsg);
263 /* Keep the udf in the hash so that we can remove it later */
264 continue;
265 }
266 new_dl=1;
267 }
268 tmp->dlhandle = dl;
269 {
270 char buf[NAME_LEN+16], *missing;
271 if ((missing= init_syms(tmp, buf)))
272 {
273 sql_print_error(ER(ER_CANT_FIND_DL_ENTRY), missing);
274 del_udf(tmp);
275 if (new_dl)
276 dlclose(dl);
277 }
278 }
279 }
280 if (error > 0)
281 sql_print_error("Got unknown error: %d", my_errno());
282 end_read_record(&read_record_info);
283 table->m_needs_reopen= TRUE; // Force close to free memory
284
285 end:
286 close_mysql_tables(new_thd);
287 delete new_thd;
288 DBUG_VOID_RETURN;
289 }
290
291
udf_free()292 void udf_free()
293 {
294 /* close all shared libraries */
295 DBUG_ENTER("udf_free");
296 for (uint idx=0 ; idx < udf_hash.records ; idx++)
297 {
298 udf_func *udf=(udf_func*) my_hash_element(&udf_hash,idx);
299 if (udf->dlhandle) // Not closed before
300 {
301 /* Mark all versions using the same handler as closed */
302 for (uint j=idx+1 ; j < udf_hash.records ; j++)
303 {
304 udf_func *tmp=(udf_func*) my_hash_element(&udf_hash,j);
305 if (udf->dlhandle == tmp->dlhandle)
306 tmp->dlhandle=0; // Already closed
307 }
308 dlclose(udf->dlhandle);
309 }
310 }
311 my_hash_free(&udf_hash);
312 free_root(&mem,MYF(0));
313 if (initialized)
314 {
315 initialized= 0;
316 mysql_rwlock_destroy(&THR_LOCK_udf);
317 }
318 DBUG_VOID_RETURN;
319 }
320
321
del_udf(udf_func * udf)322 static void del_udf(udf_func *udf)
323 {
324 DBUG_ENTER("del_udf");
325 if (!--udf->usage_count)
326 {
327 my_hash_delete(&udf_hash,(uchar*) udf);
328 using_udf_functions=udf_hash.records != 0;
329 }
330 else
331 {
332 /*
333 The functions is in use ; Rename the functions instead of removing it.
334 The functions will be automaticly removed when the least threads
335 doesn't use it anymore
336 */
337 char *name= udf->name.str;
338 size_t name_length=udf->name.length;
339 udf->name.str=(char*) "*";
340 udf->name.length=1;
341 my_hash_update(&udf_hash,(uchar*) udf,(uchar*) name,name_length);
342 }
343 DBUG_VOID_RETURN;
344 }
345
346
free_udf(udf_func * udf)347 void free_udf(udf_func *udf)
348 {
349 DBUG_ENTER("free_udf");
350
351 if (!initialized)
352 DBUG_VOID_RETURN;
353
354 mysql_rwlock_wrlock(&THR_LOCK_udf);
355 if (!--udf->usage_count)
356 {
357 /*
358 We come here when someone has deleted the udf function
359 while another thread still was using the udf
360 */
361 my_hash_delete(&udf_hash,(uchar*) udf);
362 using_udf_functions=udf_hash.records != 0;
363 if (!find_udf_dl(udf->dl))
364 dlclose(udf->dlhandle);
365 }
366 mysql_rwlock_unlock(&THR_LOCK_udf);
367 DBUG_VOID_RETURN;
368 }
369
370
371 /* This is only called if using_udf_functions != 0 */
372
find_udf(const char * name,size_t length,bool mark_used)373 udf_func *find_udf(const char *name, size_t length,bool mark_used)
374 {
375 udf_func *udf=0;
376 DBUG_ENTER("find_udf");
377
378 if (!initialized)
379 DBUG_RETURN(NULL);
380
381 /* TODO: This should be changed to reader locks someday! */
382 if (mark_used)
383 mysql_rwlock_wrlock(&THR_LOCK_udf); /* Called during fix_fields */
384 else
385 mysql_rwlock_rdlock(&THR_LOCK_udf); /* Called during parsing */
386
387 if ((udf=(udf_func*) my_hash_search(&udf_hash,(uchar*) name,
388 length ? length : strlen(name))))
389 {
390 if (!udf->dlhandle)
391 udf=0; // Could not be opened
392 else if (mark_used)
393 udf->usage_count++;
394 }
395 mysql_rwlock_unlock(&THR_LOCK_udf);
396 DBUG_RETURN(udf);
397 }
398
399
find_udf_dl(const char * dl)400 static void *find_udf_dl(const char *dl)
401 {
402 DBUG_ENTER("find_udf_dl");
403
404 /*
405 Because only the function name is hashed, we have to search trough
406 all rows to find the dl.
407 */
408 for (uint idx=0 ; idx < udf_hash.records ; idx++)
409 {
410 udf_func *udf=(udf_func*) my_hash_element(&udf_hash,idx);
411 if (!strcmp(dl, udf->dl) && udf->dlhandle != NULL)
412 DBUG_RETURN(udf->dlhandle);
413 }
414 DBUG_RETURN(0);
415 }
416
417
418 /* Assume that name && dl is already allocated */
419
add_udf(LEX_STRING * name,Item_result ret,char * dl,Item_udftype type)420 static udf_func *add_udf(LEX_STRING *name, Item_result ret, char *dl,
421 Item_udftype type)
422 {
423 if (!name || !dl || !(uint) type || (uint) type > (uint) UDFTYPE_AGGREGATE)
424 return 0;
425 udf_func *tmp= (udf_func*) alloc_root(&mem, sizeof(udf_func));
426 if (!tmp)
427 return 0;
428 memset(tmp, 0, sizeof(*tmp));
429 tmp->name = *name; //dup !!
430 tmp->dl = dl;
431 tmp->returns = ret;
432 tmp->type = type;
433 tmp->usage_count=1;
434 if (my_hash_insert(&udf_hash,(uchar*) tmp))
435 return 0;
436 using_udf_functions=1;
437 return tmp;
438 }
439
440
441 /**
442 Create a user defined function.
443
444 @note Like implementations of other DDL/DML in MySQL, this function
445 relies on the caller to close the thread tables. This is done in the
446 end of dispatch_command().
447 */
448
mysql_create_function(THD * thd,udf_func * udf)449 int mysql_create_function(THD *thd,udf_func *udf)
450 {
451 int error;
452 void *dl=0;
453 bool new_dl=0;
454 TABLE *table;
455 TABLE_LIST tables;
456 udf_func *u_d;
457 bool save_binlog_row_based;
458 DBUG_ENTER("mysql_create_function");
459
460 if (!initialized)
461 {
462 if (opt_noacl)
463 my_error(ER_CANT_INITIALIZE_UDF, MYF(0),
464 udf->name.str,
465 "UDFs are unavailable with the --skip-grant-tables option");
466 else
467 my_message(ER_OUT_OF_RESOURCES, ER(ER_OUT_OF_RESOURCES), MYF(0));
468 DBUG_RETURN(1);
469 }
470
471 /*
472 Ensure that the .dll doesn't have a path
473 This is done to ensure that only approved dll from the system
474 directories are used (to make this even remotely secure).
475 */
476 if (check_valid_path(udf->dl, strlen(udf->dl)))
477 {
478 my_message(ER_UDF_NO_PATHS, ER(ER_UDF_NO_PATHS), MYF(0));
479 DBUG_RETURN(1);
480 }
481 LEX_CSTRING udf_name_cstr= {udf->name.str, udf->name.length};
482 if (check_string_char_length(udf_name_cstr, "", NAME_CHAR_LEN,
483 system_charset_info, 1))
484 {
485 my_error(ER_TOO_LONG_IDENT, MYF(0), udf->name.str);
486 DBUG_RETURN(1);
487 }
488
489 tables.init_one_table("mysql", 5, "func", 4, "func", TL_WRITE);
490 if (!(table= open_ltable(thd, &tables, TL_WRITE, MYSQL_LOCK_IGNORE_TIMEOUT)))
491 DBUG_RETURN(1);
492
493 /*
494 Turn off row binlogging of this statement and use statement-based
495 so that all supporting tables are updated for CREATE FUNCTION command.
496 */
497 if ((save_binlog_row_based= thd->is_current_stmt_binlog_format_row()))
498 thd->clear_current_stmt_binlog_format_row();
499
500 mysql_rwlock_wrlock(&THR_LOCK_udf);
501 if ((my_hash_search(&udf_hash,(uchar*) udf->name.str, udf->name.length)))
502 {
503 my_error(ER_UDF_EXISTS, MYF(0), udf->name.str);
504 goto err;
505 }
506 if (!(dl = find_udf_dl(udf->dl)))
507 {
508 char dlpath[FN_REFLEN];
509 strxnmov(dlpath, sizeof(dlpath) - 1, opt_plugin_dir, "/", udf->dl, NullS);
510 (void) unpack_filename(dlpath, dlpath);
511
512 if (!(dl = dlopen(dlpath, RTLD_NOW)))
513 {
514 const char *errmsg;
515 int error_number= dlopen_errno;
516 DLERROR_GENERATE(errmsg, error_number);
517
518 DBUG_PRINT("error",("dlopen of %s failed, error: %d (%s)",
519 udf->dl, error_number, errmsg));
520 my_error(ER_CANT_OPEN_LIBRARY, MYF(0),
521 udf->dl, error_number, errmsg);
522 goto err;
523 }
524 new_dl=1;
525 }
526 udf->dlhandle=dl;
527 {
528 char buf[NAME_LEN+16], *missing;
529 if ((missing= init_syms(udf, buf)))
530 {
531 my_error(ER_CANT_FIND_DL_ENTRY, MYF(0), missing);
532 goto err;
533 }
534 }
535 udf->name.str=strdup_root(&mem,udf->name.str);
536 udf->dl=strdup_root(&mem,udf->dl);
537 if (!(u_d=add_udf(&udf->name,udf->returns,udf->dl,udf->type)))
538 goto err;
539 u_d->dlhandle = dl;
540 u_d->func=udf->func;
541 u_d->func_init=udf->func_init;
542 u_d->func_deinit=udf->func_deinit;
543 u_d->func_clear=udf->func_clear;
544 u_d->func_add=udf->func_add;
545
546 /* create entry in mysql.func table */
547
548 table->use_all_columns();
549 restore_record(table, s->default_values); // Default values for fields
550 table->field[0]->store(u_d->name.str, u_d->name.length, system_charset_info);
551 table->field[1]->store((longlong) u_d->returns, TRUE);
552 table->field[2]->store(u_d->dl, strlen(u_d->dl), system_charset_info);
553 if (table->s->fields >= 4) // If not old func format
554 table->field[3]->store((longlong) u_d->type, TRUE);
555 error = table->file->ha_write_row(table->record[0]);
556
557 if (error)
558 {
559 char errbuf[MYSYS_STRERROR_SIZE];
560 my_error(ER_ERROR_ON_WRITE, MYF(0), "mysql.func", error,
561 my_strerror(errbuf, sizeof(errbuf), error));
562 del_udf(u_d);
563 goto err;
564 }
565 mysql_rwlock_unlock(&THR_LOCK_udf);
566
567 /* Binlog the create function. */
568 if (write_bin_log(thd, true, thd->query().str, thd->query().length))
569 {
570 /* Restore the state of binlog format */
571 assert(!thd->is_current_stmt_binlog_format_row());
572 if (save_binlog_row_based)
573 thd->set_current_stmt_binlog_format_row();
574 DBUG_RETURN(1);
575 }
576 /* Restore the state of binlog format */
577 assert(!thd->is_current_stmt_binlog_format_row());
578 if (save_binlog_row_based)
579 thd->set_current_stmt_binlog_format_row();
580 DBUG_RETURN(0);
581
582 err:
583 if (new_dl)
584 dlclose(dl);
585 mysql_rwlock_unlock(&THR_LOCK_udf);
586 /* Restore the state of binlog format */
587 assert(!thd->is_current_stmt_binlog_format_row());
588 if (save_binlog_row_based)
589 thd->set_current_stmt_binlog_format_row();
590 DBUG_RETURN(1);
591 }
592
593
mysql_drop_function(THD * thd,const LEX_STRING * udf_name)594 int mysql_drop_function(THD *thd,const LEX_STRING *udf_name)
595 {
596 TABLE *table;
597 TABLE_LIST tables;
598 udf_func *udf;
599 char *exact_name_str;
600 size_t exact_name_len;
601 bool save_binlog_row_based;
602 int error= 1;
603 DBUG_ENTER("mysql_drop_function");
604
605 if (!initialized)
606 {
607 if (opt_noacl)
608 my_error(ER_FUNCTION_NOT_DEFINED, MYF(0), udf_name->str);
609 else
610 my_message(ER_OUT_OF_RESOURCES, ER(ER_OUT_OF_RESOURCES), MYF(0));
611 DBUG_RETURN(1);
612 }
613
614 tables.init_one_table("mysql", 5, "func", 4, "func", TL_WRITE);
615 if (!(table= open_ltable(thd, &tables, TL_WRITE, MYSQL_LOCK_IGNORE_TIMEOUT)))
616 DBUG_RETURN(1);
617
618 /*
619 Turn off row binlogging of this statement and use statement-based
620 so that all supporting tables are updated for DROP FUNCTION command.
621 */
622 if ((save_binlog_row_based= thd->is_current_stmt_binlog_format_row()))
623 thd->clear_current_stmt_binlog_format_row();
624
625 mysql_rwlock_wrlock(&THR_LOCK_udf);
626 if (!(udf=(udf_func*) my_hash_search(&udf_hash,(uchar*) udf_name->str,
627 (uint) udf_name->length)))
628 {
629 my_error(ER_FUNCTION_NOT_DEFINED, MYF(0), udf_name->str);
630 mysql_rwlock_unlock(&THR_LOCK_udf);
631 goto exit;
632 }
633 exact_name_str= udf->name.str;
634 exact_name_len= udf->name.length;
635 del_udf(udf);
636 /*
637 Close the handle if this was function that was found during boot or
638 CREATE FUNCTION and it's not in use by any other udf function
639 */
640 if (udf->dlhandle && !find_udf_dl(udf->dl))
641 dlclose(udf->dlhandle);
642 mysql_rwlock_unlock(&THR_LOCK_udf);
643
644 table->use_all_columns();
645 table->field[0]->store(exact_name_str, exact_name_len, &my_charset_bin);
646 if (!table->file->ha_index_read_idx_map(table->record[0], 0,
647 table->field[0]->ptr,
648 HA_WHOLE_KEY,
649 HA_READ_KEY_EXACT))
650 {
651 int delete_err;
652 if ((delete_err = table->file->ha_delete_row(table->record[0])))
653 table->file->print_error(delete_err, MYF(0));
654 }
655
656 /*
657 Binlog the drop function. Keep the table open and locked
658 while binlogging, to avoid binlog inconsistency.
659 */
660 if (!write_bin_log(thd, true, thd->query().str, thd->query().length))
661 error= 0;
662 exit:
663 /* Restore the state of binlog format */
664 assert(!thd->is_current_stmt_binlog_format_row());
665 if (save_binlog_row_based)
666 thd->set_current_stmt_binlog_format_row();
667 DBUG_RETURN(error);
668 }
669
670 #endif /* HAVE_DLOPEN */
671