hush/modutils/modprobe.c
Rob Landley c7ddefc062 Attempt at fixing bug 815 by upgrading bb_spawn() so that builtins are at
the start of the path.  (This should be under the same config option as
the standalone shell, but right now that's buried in the shell menu.)

Also add the ability to specify CONFIG_BUSYBOX_EXEC_PATH with /proc/self/exe
as an overrideable default.
2006-06-14 01:24:33 +00:00

887 lines
23 KiB
C

/* vi: set sw=4 ts=4: */
/*
* Modprobe written from scratch for BusyBox
*
* Copyright (c) 2002 by Robert Griebl, griebl@gmx.de
* Copyright (c) 2003 by Andrew Dennison, andrew.dennison@motec.com.au
* Copyright (c) 2005 by Jim Bauer, jfbauer@nfr.com
*
* Portions Copyright (c) 2005 by Yann E. MORIN, yann.morin.1998@anciens.enib.fr
*
* Licensed under GPLv2 or later, see file LICENSE in this tarball for details.
*/
#include "busybox.h"
#include <sys/utsname.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <getopt.h>
#include <stdlib.h>
#include <unistd.h>
#include <syslog.h>
#include <string.h>
#include <ctype.h>
#include <fcntl.h>
#include <fnmatch.h>
struct mod_opt_t { /* one-way list of options to pass to a module */
char * m_opt_val;
struct mod_opt_t * m_next;
};
struct dep_t { /* one-way list of dependency rules */
/* a dependency rule */
char * m_name; /* the module name*/
char * m_path; /* the module file path */
struct mod_opt_t * m_options; /* the module options */
int m_isalias : 1; /* the module is an alias */
int m_reserved : 15; /* stuffin' */
int m_depcnt : 16; /* the number of dependable module(s) */
char ** m_deparr; /* the list of dependable module(s) */
struct dep_t * m_next; /* the next dependency rule */
};
struct mod_list_t { /* two-way list of modules to process */
/* a module description */
char * m_name;
char * m_path;
struct mod_opt_t * m_options;
struct mod_list_t * m_prev;
struct mod_list_t * m_next;
};
static struct dep_t *depend;
#define main_options "acdklnqrst:vVC:"
#define INSERT_ALL 1 /* a */
#define DUMP_CONF_EXIT 2 /* c */
#define D_OPT_IGNORED 4 /* d */
#define AUTOCLEAN_FLG 8 /* k */
#define LIST_ALL 16 /* l */
#define SHOW_ONLY 32 /* n */
#define QUIET 64 /* q */
#define REMOVE_OPT 128 /* r */
#define DO_SYSLOG 256 /* s */
#define RESTRICT_DIR 512 /* t */
#define VERBOSE 1024 /* v */
#define VERSION_ONLY 2048 /* V */
#define CONFIG_FILE 4096 /* C */
#define autoclean (main_opts & AUTOCLEAN_FLG)
#define show_only (main_opts & SHOW_ONLY)
#define quiet (main_opts & QUIET)
#define remove_opt (main_opts & REMOVE_OPT)
#define do_syslog (main_opts & DO_SYSLOG)
#define verbose (main_opts & VERBOSE)
static int main_opts;
static int parse_tag_value ( char *buffer, char **ptag, char **pvalue )
{
char *tag, *value;
while ( isspace ( *buffer ))
buffer++;
tag = value = buffer;
while ( !isspace ( *value ))
if (!*value) return 0;
else value++;
*value++ = 0;
while ( isspace ( *value ))
value++;
if (!*value) return 0;
*ptag = tag;
*pvalue = value;
return 1;
}
/* Jump through hoops to simulate how fgets() grabs just one line at a
* time... Don't use any stdio since modprobe gets called from a kernel
* thread and stdio junk can overflow the limited stack...
*/
static char *reads ( int fd, char *buffer, size_t len )
{
int n = read ( fd, buffer, len );
if ( n > 0 ) {
char *p;
buffer [len-1] = 0;
p = strchr ( buffer, '\n' );
if ( p ) {
off_t offset;
offset = lseek ( fd, 0L, SEEK_CUR ); // Get the current file descriptor offset
lseek ( fd, offset-n + (p-buffer) + 1, SEEK_SET ); // Set the file descriptor offset to right after the \n
p[1] = 0;
}
return buffer;
}
else
return 0;
}
/*
* This function appends an option to a list
*/
static struct mod_opt_t *append_option( struct mod_opt_t *opt_list, char *opt )
{
struct mod_opt_t *ol = opt_list;
if( ol ) {
while( ol-> m_next ) {
ol = ol-> m_next;
}
ol-> m_next = xmalloc( sizeof( struct mod_opt_t ) );
ol = ol-> m_next;
} else {
ol = opt_list = xmalloc( sizeof( struct mod_opt_t ) );
}
ol-> m_opt_val = bb_xstrdup( opt );
ol-> m_next = NULL;
return opt_list;
}
#if ENABLE_FEATURE_MODPROBE_MULTIPLE_OPTIONS
/* static char* parse_command_string( char* src, char **dst );
* src: pointer to string containing argument
* dst: pointer to where to store the parsed argument
* return value: the pointer to the first char after the parsed argument,
* NULL if there was no argument parsed (only trailing spaces).
* Note that memory is allocated with bb_xstrdup when a new argument was
* parsed. Don't forget to free it!
*/
#define ARG_EMPTY 0x00
#define ARG_IN_DQUOTES 0x01
#define ARG_IN_SQUOTES 0x02
static char *parse_command_string( char *src, char **dst )
{
int opt_status = ARG_EMPTY;
char* tmp_str;
/* Dumb you, I have nothing to do... */
if( src == NULL ) return src;
/* Skip leading spaces */
while( *src == ' ' ) {
src++;
}
/* Is the end of string reached? */
if( *src == '\0' ) {
return NULL;
}
/* Reached the start of an argument
* By the way, we duplicate a little too much
* here but what is too much is freed later. */
*dst = tmp_str = bb_xstrdup( src );
/* Get to the end of that argument */
while( ( *tmp_str != '\0' )
&& ( ( *tmp_str != ' ' )
|| ( opt_status & ( ARG_IN_DQUOTES | ARG_IN_SQUOTES ) ) ) ) {
switch( *tmp_str ) {
case '\'':
if( opt_status & ARG_IN_DQUOTES ) {
/* Already in double quotes, keep current char as is */
} else {
/* shift left 1 char, until end of string: get rid of the opening/closing quotes */
memmove( tmp_str, tmp_str + 1, strlen( tmp_str ) );
/* mark me: we enter or leave single quotes */
opt_status ^= ARG_IN_SQUOTES;
/* Back one char, as we need to re-scan the new char there. */
tmp_str--;
}
break;
case '"':
if( opt_status & ARG_IN_SQUOTES ) {
/* Already in single quotes, keep current char as is */
} else {
/* shift left 1 char, until end of string: get rid of the opening/closing quotes */
memmove( tmp_str, tmp_str + 1, strlen( tmp_str ) );
/* mark me: we enter or leave double quotes */
opt_status ^= ARG_IN_DQUOTES;
/* Back one char, as we need to re-scan the new char there. */
tmp_str--;
}
break;
case '\\':
if( opt_status & ARG_IN_SQUOTES ) {
/* Between single quotes: keep as is. */
} else {
switch( *(tmp_str+1) ) {
case 'a':
case 'b':
case 't':
case 'n':
case 'v':
case 'f':
case 'r':
case '0':
/* We escaped a special character. For now, keep
* both the back-slash and the following char. */
tmp_str++; src++;
break;
default:
/* We escaped a space or a single or double quote,
* or a back-slash, or a non-escapable char. Remove
* the '\' and keep the new current char as is. */
memmove( tmp_str, tmp_str + 1, strlen( tmp_str ) );
break;
}
}
break;
/* Any other char that is special shall appear here.
* Example: $ starts a variable
case '$':
do_variable_expansion();
break;
* */
default:
/* any other char is kept as is. */
break;
}
tmp_str++; /* Go to next char */
src++; /* Go to next char to find the end of the argument. */
}
/* End of string, but still no ending quote */
if( opt_status & ( ARG_IN_DQUOTES | ARG_IN_SQUOTES ) ) {
bb_error_msg_and_die( "unterminated (single or double) quote in options list: %s", src );
}
*tmp_str++ = '\0';
*dst = xrealloc( *dst, (tmp_str - *dst ) );
return src;
}
#else
#define parse_command_string(src, dst) (0)
#endif /* ENABLE_FEATURE_MODPROBE_MULTIPLE_OPTIONS */
/*
* This function reads aliases and default module options from a configuration file
* (/etc/modprobe.conf syntax). It supports includes (only files, no directories).
*/
static void include_conf ( struct dep_t **first, struct dep_t **current, char *buffer, int buflen, int fd )
{
int continuation_line = 0;
// alias parsing is not 100% correct (no correct handling of continuation lines within an alias) !
while ( reads ( fd, buffer, buflen)) {
int l;
char *p;
p = strchr ( buffer, '#' );
if ( p )
*p = 0;
l = strlen ( buffer );
while ( l && isspace ( buffer [l-1] )) {
buffer [l-1] = 0;
l--;
}
if ( l == 0 ) {
continuation_line = 0;
continue;
}
if ( !continuation_line ) {
if (( strncmp ( buffer, "alias", 5 ) == 0 ) && isspace ( buffer [5] )) {
char *alias, *mod;
if ( parse_tag_value ( buffer + 6, &alias, &mod )) {
/* handle alias as a module dependent on the aliased module */
if ( !*current ) {
(*first) = (*current) = (struct dep_t *) xcalloc ( 1, sizeof ( struct dep_t ));
}
else {
(*current)-> m_next = (struct dep_t *) xcalloc ( 1, sizeof ( struct dep_t ));
(*current) = (*current)-> m_next;
}
(*current)-> m_name = bb_xstrdup ( alias );
(*current)-> m_isalias = 1;
if (( strcmp ( mod, "off" ) == 0 ) || ( strcmp ( mod, "null" ) == 0 )) {
(*current)-> m_depcnt = 0;
(*current)-> m_deparr = 0;
}
else {
(*current)-> m_depcnt = 1;
(*current)-> m_deparr = xmalloc ( 1 * sizeof( char * ));
(*current)-> m_deparr[0] = bb_xstrdup ( mod );
}
(*current)-> m_next = 0;
}
}
else if (( strncmp ( buffer, "options", 7 ) == 0 ) && isspace ( buffer [7] )) {
char *mod, *opt;
/* split the line in the module/alias name, and options */
if ( parse_tag_value ( buffer + 8, &mod, &opt )) {
struct dep_t *dt;
/* find the corresponding module */
for ( dt = *first; dt; dt = dt-> m_next ) {
if ( strcmp ( dt-> m_name, mod ) == 0 )
break;
}
if ( dt ) {
if ( ENABLE_FEATURE_MODPROBE_MULTIPLE_OPTIONS ) {
char* new_opt = NULL;
while( ( opt = parse_command_string( opt, &new_opt ) ) ) {
dt-> m_options = append_option( dt-> m_options, new_opt );
}
} else {
dt-> m_options = append_option( dt-> m_options, opt );
}
}
}
}
else if (( strncmp ( buffer, "include", 7 ) == 0 ) && isspace ( buffer [7] )) {
int fdi; char *filename = buffer + 8;
while ( isspace ( *filename ))
filename++;
if (( fdi = open ( filename, O_RDONLY )) >= 0 ) {
include_conf(first, current, buffer, buflen, fdi);
close(fdi);
}
}
}
}
}
/*
* This function builds a list of dependency rules from /lib/modules/`uname -r`\modules.dep.
* It then fills every modules and aliases with their default options, found by parsing
* modprobe.conf (or modules.conf, or conf.modules).
*/
static struct dep_t *build_dep ( void )
{
int fd;
struct utsname un;
struct dep_t *first = 0;
struct dep_t *current = 0;
char buffer[2048];
char *filename;
int continuation_line = 0;
int k_version;
k_version = 0;
if ( uname ( &un ))
bb_error_msg_and_die("can't determine kernel version");
if (un.release[0] == '2') {
k_version = un.release[2] - '0';
}
filename = bb_xasprintf("/lib/modules/%s/modules.dep", un.release );
fd = open ( filename, O_RDONLY );
if (ENABLE_FEATURE_CLEAN_UP)
free(filename);
if (fd < 0) {
/* Ok, that didn't work. Fall back to looking in /lib/modules */
if (( fd = open ( "/lib/modules/modules.dep", O_RDONLY )) < 0 ) {
return 0;
}
}
while ( reads ( fd, buffer, sizeof( buffer ))) {
int l = strlen ( buffer );
char *p = 0;
while ( l > 0 && isspace ( buffer [l-1] )) {
buffer [l-1] = 0;
l--;
}
if ( l == 0 ) {
continuation_line = 0;
continue;
}
/* Is this a new module dep description? */
if ( !continuation_line ) {
/* find the dep beginning */
char *col = strchr ( buffer, ':' );
char *dot = col;
if ( col ) {
/* This line is a dep description */
char *mods;
char *modpath;
char *mod;
/* Find the beginning of the module file name */
*col = 0;
mods = strrchr ( buffer, '/' );
if ( !mods )
mods = buffer; /* no path for this module */
else
mods++; /* there was a path for this module... */
/* find the path of the module */
modpath = strchr ( buffer, '/' ); /* ... and this is the path */
if ( !modpath )
modpath = buffer; /* module with no path */
/* find the end of the module name in the file name */
if ( ENABLE_FEATURE_2_6_MODULES &&
(k_version > 4) && ( *(col-3) == '.' ) &&
( *(col-2) == 'k' ) && ( *(col-1) == 'o' ) )
dot = col - 3;
else
if (( *(col-2) == '.' ) && ( *(col-1) == 'o' ))
dot = col - 2;
mod = bb_xstrndup ( mods, dot - mods );
/* enqueue new module */
if ( !current ) {
first = current = (struct dep_t *) xmalloc ( sizeof ( struct dep_t ));
}
else {
current-> m_next = (struct dep_t *) xmalloc ( sizeof ( struct dep_t ));
current = current-> m_next;
}
current-> m_name = mod;
current-> m_path = bb_xstrdup(modpath);
current-> m_options = NULL;
current-> m_isalias = 0;
current-> m_depcnt = 0;
current-> m_deparr = 0;
current-> m_next = 0;
p = col + 1;
}
else
/* this line is not a dep description */
p = 0;
}
else
/* It's a dep description continuation */
p = buffer;
while ( p && *p && isblank(*p))
p++;
/* p points to the first dependable module; if NULL, no dependable module */
if ( p && *p ) {
char *end = &buffer [l-1];
char *deps;
char *dep;
char *next;
int ext = 0;
while ( isblank ( *end ) || ( *end == '\\' ))
end--;
do
{
/* search the end of the dependency */
next = strchr (p, ' ' );
if (next)
{
*next = 0;
next--;
}
else
next = end;
/* find the beginning of the module file name */
deps = strrchr ( p, '/' );
if ( !deps || ( deps < p )) {
deps = p;
while ( isblank ( *deps ))
deps++;
}
else
deps++;
/* find the end of the module name in the file name */
if ( ENABLE_FEATURE_2_6_MODULES &&
(k_version > 4) && ( *(next-2) == '.' ) &&
( *(next-1) == 'k' ) && ( *next == 'o' ) )
ext = 3;
else
if (( *(next-1) == '.' ) && ( *next == 'o' ))
ext = 2;
/* Cope with blank lines */
if ((next-deps-ext+1) <= 0)
continue;
dep = bb_xstrndup ( deps, next - deps - ext + 1 );
/* Add the new dependable module name */
current-> m_depcnt++;
current-> m_deparr = (char **) xrealloc ( current-> m_deparr,
sizeof ( char *) * current-> m_depcnt );
current-> m_deparr [current-> m_depcnt - 1] = dep;
p = next + 2;
} while (next < end);
}
/* is there other dependable module(s) ? */
if ( buffer [l-1] == '\\' )
continuation_line = 1;
else
continuation_line = 0;
}
close ( fd );
if (!ENABLE_FEATURE_2_6_MODULES
|| ( fd = open ( "/etc/modprobe.conf", O_RDONLY )) < 0 )
if (( fd = open ( "/etc/modules.conf", O_RDONLY )) < 0 )
if (( fd = open ( "/etc/conf.modules", O_RDONLY )) < 0 )
return first;
include_conf (&first, &current, buffer, sizeof(buffer), fd);
close(fd);
filename = bb_xasprintf("/lib/modules/%s/modules.alias", un.release);
fd = open ( filename, O_RDONLY );
if (ENABLE_FEATURE_CLEAN_UP)
free(filename);
if (fd < 0) {
/* Ok, that didn't work. Fall back to looking in /lib/modules */
if (( fd = open ( "/lib/modules/modules.alias", O_RDONLY )) < 0 ) {
return first;
}
}
include_conf (&first, &current, buffer, sizeof(buffer), fd);
close(fd);
return first;
}
/* return 1 = loaded, 0 = not loaded, -1 = can't tell */
static int already_loaded (const char *name)
{
int fd;
char buffer[4096];
fd = open ("/proc/modules", O_RDONLY);
if (fd < 0)
return -1;
while ( reads ( fd, buffer, sizeof( buffer ))) {
char *p;
p = strchr (buffer, ' ');
if (p) {
*p = 0;
for( p = buffer; ENABLE_FEATURE_2_6_MODULES && *p; p++ ) {
*p = ((*p)=='-')?'_':*p;
}
if (strcmp (name, buffer) == 0) {
close (fd);
return 1;
}
}
}
close (fd);
return 0;
}
static int mod_process ( struct mod_list_t *list, int do_insert )
{
int rc = 0;
char **argv = NULL;
struct mod_opt_t *opts;
int argc_malloc; /* never used when CONFIG_FEATURE_CLEAN_UP not defined */
int argc;
while ( list ) {
argc = 0;
if( ENABLE_FEATURE_CLEAN_UP )
argc_malloc = 0;
/* If CONFIG_FEATURE_CLEAN_UP is not defined, then we leak memory
* each time we allocate memory for argv.
* But it is (quite) small amounts of memory that leak each
* time a module is loaded, and it is reclaimed when modprobe
* exits anyway (even when standalone shell?).
* This could become a problem when loading a module with LOTS of
* dependencies, with LOTS of options for each dependencies, with
* very little memory on the target... But in that case, the module
* would not load because there is no more memory, so there's no
* problem. */
/* enough for minimal insmod (5 args + NULL) or rmmod (3 args + NULL) */
argv = (char**) malloc( 6 * sizeof( char* ) );
if ( do_insert ) {
if (already_loaded (list->m_name) != 1) {
argv[argc++] = "insmod";
if (do_syslog)
argv[argc++] = "-s";
if (autoclean)
argv[argc++] = "-k";
if (quiet)
argv[argc++] = "-q";
else if(verbose) /* verbose and quiet are mutually exclusive */
argv[argc++] = "-v";
argv[argc++] = list-> m_path;
if( ENABLE_FEATURE_CLEAN_UP )
argc_malloc = argc;
opts = list-> m_options;
while( opts ) {
/* Add one more option */
argc++;
argv = (char**) xrealloc( argv, ( argc + 1 ) * sizeof( char* ) );
argv[argc-1] = opts-> m_opt_val;
opts = opts-> m_next;
}
}
} else {
/* modutils uses short name for removal */
if (already_loaded (list->m_name) != 0) {
argv[argc++] = "rmmod";
if (do_syslog)
argv[argc++] = "-s";
argv[argc++] = list->m_name;
if( ENABLE_FEATURE_CLEAN_UP )
argc_malloc = argc;
}
}
argv[argc] = NULL;
if (argc) {
if (verbose) {
printf("%s module %s\n", do_insert?"Loading":"Unloading", list-> m_name );
}
if (!show_only) {
int rc2 = wait4pid(bb_spawn(argv));
if (do_insert) {
rc = rc2; /* only last module matters */
}
else if (!rc2) {
rc = 0; /* success if remove any mod */
}
}
if( ENABLE_FEATURE_CLEAN_UP )
/* the last value in the array has index == argc, but
* it is the terminating NULL, so we must not free it. */
while( argc_malloc < argc ) {
free( argv[argc_malloc++] );
}
}
if( ENABLE_FEATURE_CLEAN_UP ) {
free( argv );
argv = NULL;
}
list = do_insert ? list-> m_prev : list-> m_next;
}
return (show_only) ? 0 : rc;
}
/*
* Builds the dependency list (aka stack) of a module.
* head: the highest module in the stack (last to insmod, first to rmmod)
* tail: the lowest module in the stack (first to insmod, last to rmmod)
*/
static void check_dep ( char *mod, struct mod_list_t **head, struct mod_list_t **tail )
{
struct mod_list_t *find;
struct dep_t *dt;
struct mod_opt_t *opt = 0;
char *path = 0;
/* Search for the given module name amongst all dependency rules.
* The module name in a dependency rule can be a shell pattern,
* so try to match the given module name against such a pattern.
* Of course if the name in the dependency rule is a plain string,
* then we consider it a pattern, and matching will still work. */
for ( dt = depend; dt; dt = dt-> m_next ) {
if ( fnmatch ( dt-> m_name, mod, 0 ) == 0) {
break;
}
}
if( !dt ) {
bb_error_msg ("module %s not found.", mod);
return;
}
// resolve alias names
while ( dt-> m_isalias ) {
if ( dt-> m_depcnt == 1 ) {
struct dep_t *adt;
for ( adt = depend; adt; adt = adt-> m_next ) {
if ( strcmp ( adt-> m_name, dt-> m_deparr [0] ) == 0 )
break;
}
if ( adt ) {
/* This is the module we are aliased to */
struct mod_opt_t *opts = dt-> m_options;
/* Option of the alias are appended to the options of the module */
while( opts ) {
adt-> m_options = append_option( adt-> m_options, opts-> m_opt_val );
opts = opts-> m_next;
}
dt = adt;
}
else {
bb_error_msg ("module %s not found.", mod);
return;
}
}
else {
bb_error_msg ("Bad alias %s", dt-> m_name);
return;
}
}
mod = dt-> m_name;
path = dt-> m_path;
opt = dt-> m_options;
// search for duplicates
for ( find = *head; find; find = find-> m_next ) {
if ( !strcmp ( mod, find-> m_name )) {
// found -> dequeue it
if ( find-> m_prev )
find-> m_prev-> m_next = find-> m_next;
else
*head = find-> m_next;
if ( find-> m_next )
find-> m_next-> m_prev = find-> m_prev;
else
*tail = find-> m_prev;
break; // there can be only one duplicate
}
}
if ( !find ) { // did not find a duplicate
find = (struct mod_list_t *) xmalloc ( sizeof(struct mod_list_t));
find-> m_name = mod;
find-> m_path = path;
find-> m_options = opt;
}
// enqueue at tail
if ( *tail )
(*tail)-> m_next = find;
find-> m_prev = *tail;
find-> m_next = 0;
if ( !*head )
*head = find;
*tail = find;
if ( dt ) {
int i;
/* Add all dependable module for that new module */
for ( i = 0; i < dt-> m_depcnt; i++ )
check_dep ( dt-> m_deparr [i], head, tail );
}
}
static int mod_insert ( char *mod, int argc, char **argv )
{
struct mod_list_t *tail = 0;
struct mod_list_t *head = 0;
int rc;
// get dep list for module mod
check_dep ( mod, &head, &tail );
if ( head && tail ) {
if( argc ) {
int i;
// append module args
for ( i = 0; i < argc; i++ )
head->m_options = append_option( head->m_options, argv[i] );
}
// process tail ---> head
rc = mod_process ( tail, 1 );
}
else
rc = 1;
return rc;
}
static int mod_remove ( char *mod )
{
int rc;
static struct mod_list_t rm_a_dummy = { "-a", NULL, NULL, NULL, NULL };
struct mod_list_t *head = 0;
struct mod_list_t *tail = 0;
if ( mod )
check_dep ( mod, &head, &tail );
else // autoclean
head = tail = &rm_a_dummy;
if ( head && tail )
rc = mod_process ( head, 0 ); // process head ---> tail
else
rc = 1;
return rc;
}
int modprobe_main(int argc, char** argv)
{
int rc = EXIT_SUCCESS;
char *unused;
bb_opt_complementally = "?V-:q-v:v-q";
main_opts = bb_getopt_ulflags(argc, argv, "acdklnqrst:vVC:",
&unused, &unused);
if((main_opts & (DUMP_CONF_EXIT | LIST_ALL)))
return EXIT_SUCCESS;
if((main_opts & (RESTRICT_DIR | CONFIG_FILE)))
bb_error_msg_and_die("-t and -C not supported");
depend = build_dep ( );
if ( !depend )
bb_error_msg_and_die ( "could not parse modules.dep" );
if (remove_opt) {
do {
if (mod_remove ( optind < argc ?
argv [optind] : NULL )) {
bb_error_msg ("failed to remove module %s",
argv [optind] );
rc = EXIT_FAILURE;
}
} while ( ++optind < argc );
} else {
if (optind >= argc)
bb_error_msg_and_die ( "No module or pattern provided" );
if ( mod_insert ( argv [optind], argc - optind - 1, argv + optind + 1 ))
bb_error_msg_and_die ( "failed to load module %s", argv [optind] );
}
/* Here would be a good place to free up memory allocated during the dependencies build. */
return rc;
}