mirror of
https://sourceware.org/git/glibc.git
synced 2025-01-11 11:50:06 +00:00
nss: Implement <nss_database.h>
This code manages the mappings of the available databases in NSS (i.e. passwd, hosts, netgroup, etc) with the actions that should be taken to do a query on those databases. This is the main API between query functions scattered throughout glibc and the underlying code (actions, modules, etc). Reviewed-by: Siddhesh Poyarekar <siddhesh@sourceware.org>
This commit is contained in:
parent
fd5b9b4458
commit
fa78feca47
@ -31,7 +31,7 @@ routines = nsswitch getnssent getnssent_r digits_dots \
|
||||
compat-lookup nss_hash nss_files_fopen \
|
||||
nss_readline nss_parse_line_result \
|
||||
nss_fgetent_r nss_module nss_action \
|
||||
nss_action_parse
|
||||
nss_action_parse nss_database
|
||||
|
||||
# These are the databases that go through nss dispatch.
|
||||
# Caution: if you add a database here, you must add its real name
|
||||
|
433
nss/nss_database.c
Normal file
433
nss/nss_database.c
Normal file
@ -0,0 +1,433 @@
|
||||
/* Mapping NSS services to action lists.
|
||||
Copyright (C) 2020 Free Software Foundation, Inc.
|
||||
This file is part of the GNU C Library.
|
||||
|
||||
The GNU C Library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
The GNU C Library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with the GNU C Library; if not, see
|
||||
<https://www.gnu.org/licenses/>. */
|
||||
|
||||
#include "nss_database.h"
|
||||
|
||||
#include <allocate_once.h>
|
||||
#include <array_length.h>
|
||||
#include <assert.h>
|
||||
#include <atomic.h>
|
||||
#include <ctype.h>
|
||||
#include <file_change_detection.h>
|
||||
#include <libc-lock.h>
|
||||
#include <netdb.h>
|
||||
#include <stdio_ext.h>
|
||||
#include <string.h>
|
||||
|
||||
struct nss_database_state
|
||||
{
|
||||
struct nss_database_data data;
|
||||
__libc_lock_define (, lock);
|
||||
};
|
||||
|
||||
|
||||
/* Global NSS database state. Underlying type is "struct
|
||||
nss_database_state *" but the allocate_once API requires
|
||||
"void *". */
|
||||
static void *global_database_state;
|
||||
|
||||
/* Allocate and return pointer to nss_database_state object or
|
||||
on failure return NULL. */
|
||||
static void *
|
||||
global_state_allocate (void *closure)
|
||||
{
|
||||
struct nss_database_state *result = malloc (sizeof (*result));
|
||||
if (result != NULL)
|
||||
{
|
||||
result->data.nsswitch_conf.size = -1; /* Force reload. */
|
||||
memset (result->data.services, 0, sizeof (result->data.services));
|
||||
result->data.initialized = true;
|
||||
result->data.reload_disabled = false;
|
||||
__libc_lock_init (result->lock);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Return pointer to global NSS database state, allocating as
|
||||
required, or returning NULL on failure. */
|
||||
static struct nss_database_state *
|
||||
nss_database_state_get (void)
|
||||
{
|
||||
return allocate_once (&global_database_state, global_state_allocate,
|
||||
NULL, NULL);
|
||||
}
|
||||
|
||||
/* Database default selections. nis/compat mappings get turned into
|
||||
"files" for !LINK_OBSOLETE_NSL configurations. */
|
||||
enum nss_database_default
|
||||
{
|
||||
nss_database_default_defconfig = 0, /* "nis [NOTFOUND=return] files". */
|
||||
nss_database_default_compat, /* "compat [NOTFOUND=return] files". */
|
||||
nss_database_default_dns, /* "dns [!UNAVAIL=return] files". */
|
||||
nss_database_default_files, /* "files". */
|
||||
nss_database_default_nis, /* "nis". */
|
||||
nss_database_default_nis_nisplus, /* "nis nisplus". */
|
||||
nss_database_default_none, /* Empty list. */
|
||||
|
||||
NSS_DATABASE_DEFAULT_COUNT /* Number of defaults. */
|
||||
};
|
||||
|
||||
/* Databases not listed default to nss_database_default_defconfig. */
|
||||
static const char per_database_defaults[NSS_DATABASE_COUNT] =
|
||||
{
|
||||
[nss_database_group] = nss_database_default_compat,
|
||||
[nss_database_gshadow] = nss_database_default_files,
|
||||
[nss_database_hosts] = nss_database_default_dns,
|
||||
[nss_database_initgroups] = nss_database_default_none,
|
||||
[nss_database_networks] = nss_database_default_dns,
|
||||
[nss_database_passwd] = nss_database_default_compat,
|
||||
[nss_database_publickey] = nss_database_default_nis_nisplus,
|
||||
[nss_database_shadow] = nss_database_default_compat,
|
||||
};
|
||||
|
||||
struct nss_database_default_cache
|
||||
{
|
||||
nss_action_list caches[NSS_DATABASE_DEFAULT_COUNT];
|
||||
};
|
||||
|
||||
static bool
|
||||
nss_database_select_default (struct nss_database_default_cache *cache,
|
||||
enum nss_database db, nss_action_list *result)
|
||||
{
|
||||
enum nss_database_default def = per_database_defaults[db];
|
||||
*result = cache->caches[def];
|
||||
if (*result != NULL)
|
||||
return true;
|
||||
|
||||
/* Determine the default line string. */
|
||||
const char *line;
|
||||
switch (def)
|
||||
{
|
||||
#ifdef LINK_OBSOLETE_NSL
|
||||
case nss_database_default_defconfig:
|
||||
line = "nis [NOTFOUND=return] files";
|
||||
break;
|
||||
case nss_database_default_compat:
|
||||
line = "compat [NOTFOUND=return] files";
|
||||
break;
|
||||
#endif
|
||||
|
||||
case nss_database_default_dns:
|
||||
line = "dns [!UNAVAIL=return] files";
|
||||
break;
|
||||
|
||||
case nss_database_default_files:
|
||||
#ifndef LINK_OBSOLETE_NSL
|
||||
case nss_database_default_defconfig:
|
||||
case nss_database_default_compat:
|
||||
#endif
|
||||
line = "files";
|
||||
break;
|
||||
|
||||
case nss_database_default_nis:
|
||||
line = "nis";
|
||||
break;
|
||||
|
||||
case nss_database_default_nis_nisplus:
|
||||
line = "nis nisplus";
|
||||
break;
|
||||
|
||||
case nss_database_default_none:
|
||||
/* Very special case: Leave *result as NULL. */
|
||||
return true;
|
||||
|
||||
case NSS_DATABASE_DEFAULT_COUNT:
|
||||
__builtin_unreachable ();
|
||||
}
|
||||
if (def < 0 || def >= NSS_DATABASE_DEFAULT_COUNT)
|
||||
/* Tell GCC that line is initialized. */
|
||||
__builtin_unreachable ();
|
||||
|
||||
*result = __nss_action_parse (line);
|
||||
if (*result == NULL)
|
||||
{
|
||||
assert (errno == ENOMEM);
|
||||
return false;
|
||||
}
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
/* database_name must be large enough for each individual name plus a
|
||||
null terminator. */
|
||||
typedef char database_name[11];
|
||||
#define DEFINE_DATABASE(name) \
|
||||
_Static_assert (sizeof (#name) <= sizeof (database_name), #name);
|
||||
#include "databases.def"
|
||||
#undef DEFINE_DATABASE
|
||||
|
||||
static const database_name nss_database_name_array[] =
|
||||
{
|
||||
#define DEFINE_DATABASE(name) #name,
|
||||
#include "databases.def"
|
||||
#undef DEFINE_DATABASE
|
||||
};
|
||||
|
||||
static int
|
||||
name_search (const void *left, const void *right)
|
||||
{
|
||||
return strcmp (left, right);
|
||||
}
|
||||
|
||||
static int
|
||||
name_to_database_index (const char *name)
|
||||
{
|
||||
database_name *name_entry = bsearch (name, nss_database_name_array,
|
||||
array_length (nss_database_name_array),
|
||||
sizeof (database_name), name_search);
|
||||
if (name_entry == NULL)
|
||||
return -1;
|
||||
return name_entry - nss_database_name_array;
|
||||
}
|
||||
|
||||
static bool
|
||||
process_line (struct nss_database_data *data, char *line)
|
||||
{
|
||||
/* Ignore leading white spaces. ATTENTION: this is different from
|
||||
what is implemented in Solaris. The Solaris man page says a line
|
||||
beginning with a white space character is ignored. We regard
|
||||
this as just another misfeature in Solaris. */
|
||||
while (isspace (line[0]))
|
||||
++line;
|
||||
|
||||
/* Recognize `<database> ":"'. */
|
||||
char *name = line;
|
||||
while (line[0] != '\0' && !isspace (line[0]) && line[0] != ':')
|
||||
++line;
|
||||
if (line[0] == '\0' || name == line)
|
||||
/* Syntax error. Skip this line. */
|
||||
return true;
|
||||
*line++ = '\0';
|
||||
|
||||
int db = name_to_database_index (name);
|
||||
if (db < 0)
|
||||
/* Not our database e.g. sudoers, automount, etc. */
|
||||
return true;
|
||||
|
||||
nss_action_list result = __nss_action_parse (line);
|
||||
if (result == NULL)
|
||||
return false;
|
||||
data->services[db] = result;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Iterate over the lines in FP, parse them, and store them in DATA.
|
||||
Return false on memory allocation failure, true on success. */
|
||||
static bool
|
||||
nss_database_reload_1 (struct nss_database_data *data, FILE *fp)
|
||||
{
|
||||
char *line = NULL;
|
||||
size_t line_allocated = 0;
|
||||
bool result = false;
|
||||
|
||||
while (true)
|
||||
{
|
||||
ssize_t ret = __getline (&line, &line_allocated, fp);
|
||||
if (ferror_unlocked (fp))
|
||||
break;
|
||||
if (feof_unlocked (fp))
|
||||
{
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
assert (ret > 0);
|
||||
(void) ret; /* For NDEBUG builds. */
|
||||
|
||||
if (!process_line (data, line))
|
||||
break;
|
||||
}
|
||||
|
||||
free (line);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool
|
||||
nss_database_reload (struct nss_database_data *staging,
|
||||
struct file_change_detection *initial)
|
||||
{
|
||||
FILE *fp = fopen (_PATH_NSSWITCH_CONF, "rce");
|
||||
if (fp == NULL)
|
||||
switch (errno)
|
||||
{
|
||||
case EACCES:
|
||||
case EISDIR:
|
||||
case ELOOP:
|
||||
case ENOENT:
|
||||
case ENOTDIR:
|
||||
case EPERM:
|
||||
/* Ignore these errors. They are persistent errors caused
|
||||
by file system contents. */
|
||||
break;
|
||||
default:
|
||||
/* Other errors refer to resource allocation problems and
|
||||
need to be handled by the application. */
|
||||
return false;
|
||||
}
|
||||
else
|
||||
/* No other threads have access to fp. */
|
||||
__fsetlocking (fp, FSETLOCKING_BYCALLER);
|
||||
|
||||
bool ok = true;
|
||||
if (fp != NULL)
|
||||
ok = nss_database_reload_1 (staging, fp);
|
||||
|
||||
/* Apply defaults. */
|
||||
if (ok)
|
||||
{
|
||||
struct nss_database_default_cache cache = { };
|
||||
for (int i = 0; i < NSS_DATABASE_COUNT; ++i)
|
||||
if (staging->services[i] == NULL)
|
||||
{
|
||||
ok = nss_database_select_default (&cache, i,
|
||||
&staging->services[i]);
|
||||
if (!ok)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok)
|
||||
ok = __file_change_detection_for_fp (&staging->nsswitch_conf, fp);
|
||||
|
||||
if (fp != NULL)
|
||||
{
|
||||
int saved_errno = errno;
|
||||
fclose (fp);
|
||||
__set_errno (saved_errno);
|
||||
}
|
||||
|
||||
if (ok && !__file_is_unchanged (&staging->nsswitch_conf, initial))
|
||||
/* Reload is required because the file changed while reading. */
|
||||
staging->nsswitch_conf.size = -1;
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool
|
||||
nss_database_check_reload_and_get (struct nss_database_state *local,
|
||||
nss_action_list *result,
|
||||
enum nss_database database_index)
|
||||
{
|
||||
/* Acquire MO is needed because the thread that sets reload_disabled
|
||||
may have loaded the configuration first, so synchronize with the
|
||||
Release MO store there. */
|
||||
if (atomic_load_acquire (&local->data.reload_disabled))
|
||||
/* No reload, so there is no error. */
|
||||
return true;
|
||||
|
||||
struct file_change_detection initial;
|
||||
if (!__file_change_detection_for_path (&initial, _PATH_NSSWITCH_CONF))
|
||||
return false;
|
||||
|
||||
__libc_lock_lock (local->lock);
|
||||
if (__file_is_unchanged (&initial, &local->data.nsswitch_conf))
|
||||
{
|
||||
/* Configuration is up-to-date. Read it and return it to the
|
||||
caller. */
|
||||
*result = local->data.services[database_index];
|
||||
__libc_lock_unlock (local->lock);
|
||||
return true;
|
||||
}
|
||||
__libc_lock_unlock (local->lock);
|
||||
|
||||
/* Avoid overwriting the global configuration until we have loaded
|
||||
everything successfully. Otherwise, if the file change
|
||||
information changes back to what is in the global configuration,
|
||||
the lookups would use the partially-written configuration. */
|
||||
struct nss_database_data staging = { .initialized = true, };
|
||||
|
||||
bool ok = nss_database_reload (&staging, &initial);
|
||||
|
||||
if (ok)
|
||||
{
|
||||
__libc_lock_lock (local->lock);
|
||||
|
||||
/* See above for memory order. */
|
||||
if (!atomic_load_acquire (&local->data.reload_disabled))
|
||||
/* This may go back in time if another thread beats this
|
||||
thread with the update, but in this case, a reload happens
|
||||
on the next NSS call. */
|
||||
local->data = staging;
|
||||
|
||||
*result = local->data.services[database_index];
|
||||
__libc_lock_unlock (local->lock);
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool
|
||||
__nss_database_get (enum nss_database db, nss_action_list *actions)
|
||||
{
|
||||
struct nss_database_state *local = nss_database_state_get ();
|
||||
return nss_database_check_reload_and_get (local, actions, db);
|
||||
}
|
||||
|
||||
nss_action_list
|
||||
__nss_database_get_noreload (enum nss_database db)
|
||||
{
|
||||
/* There must have been a previous __nss_database_get call. */
|
||||
struct nss_database_state *local = atomic_load_acquire (&global_database_state);
|
||||
assert (local != NULL);
|
||||
|
||||
__libc_lock_lock (local->lock);
|
||||
nss_action_list result = local->data.services[db];
|
||||
__libc_lock_unlock (local->lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
void __libc_freeres_fn_section
|
||||
__nss_database_freeres (void)
|
||||
{
|
||||
free (global_database_state);
|
||||
global_database_state = NULL;
|
||||
}
|
||||
|
||||
void
|
||||
__nss_database_fork_prepare_parent (struct nss_database_data *data)
|
||||
{
|
||||
/* Do not use allocate_once to trigger loading unnecessarily. */
|
||||
struct nss_database_state *local = atomic_load_acquire (&global_database_state);
|
||||
if (local == NULL)
|
||||
data->initialized = false;
|
||||
else
|
||||
{
|
||||
/* Make a copy of the configuration. This approach was chosen
|
||||
because it avoids acquiring the lock during the actual
|
||||
fork. */
|
||||
__libc_lock_lock (local->lock);
|
||||
*data = local->data;
|
||||
__libc_lock_unlock (local->lock);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
__nss_database_fork_subprocess (struct nss_database_data *data)
|
||||
{
|
||||
struct nss_database_state *local = atomic_load_acquire (&global_database_state);
|
||||
if (data->initialized)
|
||||
{
|
||||
/* Restore the state at the point of the fork. */
|
||||
assert (local != NULL);
|
||||
local->data = *data;
|
||||
__libc_lock_init (local->lock);
|
||||
}
|
||||
else if (local != NULL)
|
||||
/* The NSS configuration was loaded concurrently during fork. We
|
||||
do not know its state, so we need to discard it. */
|
||||
global_database_state = NULL;
|
||||
}
|
88
nss/nss_database.h
Normal file
88
nss/nss_database.h
Normal file
@ -0,0 +1,88 @@
|
||||
/* Mapping NSS services to action lists.
|
||||
Copyright (C) 2020 Free Software Foundation, Inc.
|
||||
This file is part of the GNU C Library.
|
||||
|
||||
The GNU C Library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
The GNU C Library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with the GNU C Library; if not, see
|
||||
<https://www.gnu.org/licenses/>. */
|
||||
|
||||
#ifndef _NSS_DATABASE_H
|
||||
#define _NSS_DATABASE_H
|
||||
|
||||
#include <file_change_detection.h>
|
||||
|
||||
/* Each "line" in nsswitch.conf maps a supported database (example:
|
||||
passwd) to one or more name service providers (example: files dns).
|
||||
Internally, each name service provider (example: dns) is a
|
||||
dynamically loadable module (i.e. libnss_dns.so), managed by
|
||||
nss_module.h. The sequence of providers and rules (example: files
|
||||
[SUCCESS=RETURN] dns) is mapped by nss_action.h to a cached entry
|
||||
which encodes the sequence of modules and rules. Keeping track of
|
||||
all supported databases and their corresponding actions is done
|
||||
here.
|
||||
|
||||
The key entry is __nss_database_get, which provides a set of
|
||||
actions which can be used with nss_lookup_function() and
|
||||
nss_next(). Callers should assume that these functions are fast,
|
||||
and should not cache the result longer than needed. */
|
||||
|
||||
#include "nss_action.h"
|
||||
|
||||
/* The enumeration literal in enum nss_database for the database NAME
|
||||
(e.g., nss_database_hosts for hosts). */
|
||||
#define NSS_DATABASE_LITERAL(name) nss_database_##name
|
||||
|
||||
enum nss_database
|
||||
{
|
||||
#define DEFINE_DATABASE(name) NSS_DATABASE_LITERAL (name),
|
||||
#include "databases.def"
|
||||
#undef DEFINE_DATABASE
|
||||
|
||||
/* Total number of databases. */
|
||||
NSS_DATABASE_COUNT
|
||||
};
|
||||
|
||||
|
||||
/* Looks up the action list for DB and stores it in *ACTIONS. Returns
|
||||
true on success or false on failure. Success can mean that
|
||||
*ACTIONS is NULL. */
|
||||
bool __nss_database_get (enum nss_database db, nss_action_list *actions)
|
||||
attribute_hidden;
|
||||
|
||||
/* Like __nss_database_get, but does not reload /etc/nsswitch.conf
|
||||
from disk. This assumes that there has been a previous successful
|
||||
__nss_database_get call (which may not have returned any data). */
|
||||
nss_action_list __nss_database_get_noreload (enum nss_database db)
|
||||
attribute_hidden;
|
||||
|
||||
/* Called from __libc_freeres. */
|
||||
void __nss_database_freeres (void) attribute_hidden;
|
||||
|
||||
/* Internal type. Exposed only for fork handling purposes. */
|
||||
struct nss_database_data
|
||||
{
|
||||
struct file_change_detection nsswitch_conf;
|
||||
nss_action_list services[NSS_DATABASE_COUNT];
|
||||
int reload_disabled; /* Actually bool; int for atomic access. */
|
||||
bool initialized;
|
||||
};
|
||||
|
||||
/* Called by fork in the parent process, before forking. */
|
||||
void __nss_database_fork_prepare_parent (struct nss_database_data *data)
|
||||
attribute_hidden;
|
||||
|
||||
/* Called by fork in the new subprocess, after forking. */
|
||||
void __nss_database_fork_subprocess (struct nss_database_data *data)
|
||||
attribute_hidden;
|
||||
|
||||
#endif /* _NSS_DATABASE_H */
|
@ -28,6 +28,7 @@
|
||||
#include "hurdmalloc.h" /* XXX */
|
||||
#include <tls.h>
|
||||
#include <malloc/malloc-internal.h>
|
||||
#include <nss/nss_database.h>
|
||||
|
||||
#undef __fork
|
||||
|
||||
@ -68,6 +69,7 @@ __fork (void)
|
||||
size_t i;
|
||||
error_t err;
|
||||
struct hurd_sigstate *volatile ss;
|
||||
struct nss_database_data nss_database_data;
|
||||
|
||||
RUN_HOOK (_hurd_atfork_prepare_hook, ());
|
||||
|
||||
@ -109,6 +111,9 @@ __fork (void)
|
||||
/* Run things that prepare for forking before we create the task. */
|
||||
RUN_HOOK (_hurd_fork_prepare_hook, ());
|
||||
|
||||
call_function_static_weak (__nss_database_fork_prepare_parent,
|
||||
&nss_database_data);
|
||||
|
||||
/* Lock things that want to be locked before we fork. */
|
||||
{
|
||||
void *const *p;
|
||||
@ -666,6 +671,9 @@ __fork (void)
|
||||
_hurd_malloc_fork_child ();
|
||||
call_function_static_weak (__malloc_fork_unlock_child);
|
||||
|
||||
call_function_static_weak (__nss_database_fork_subprocess,
|
||||
&nss_database_data);
|
||||
|
||||
/* Run things that want to run in the child task to set up. */
|
||||
RUN_HOOK (_hurd_fork_child_hook, ());
|
||||
|
||||
|
@ -32,6 +32,7 @@
|
||||
#include <arch-fork.h>
|
||||
#include <futex-internal.h>
|
||||
#include <malloc/malloc-internal.h>
|
||||
#include <nss/nss_database.h>
|
||||
|
||||
static void
|
||||
fresetlockfiles (void)
|
||||
@ -57,6 +58,8 @@ __libc_fork (void)
|
||||
|
||||
__run_fork_handlers (atfork_run_prepare, multiple_threads);
|
||||
|
||||
struct nss_database_data nss_database_data;
|
||||
|
||||
/* If we are not running multiple threads, we do not have to
|
||||
preserve lock state. If fork runs from a signal handler, only
|
||||
async-signal-safe functions can be used in the child. These data
|
||||
@ -64,6 +67,9 @@ __libc_fork (void)
|
||||
not matter if fork was called from a signal handler. */
|
||||
if (multiple_threads)
|
||||
{
|
||||
call_function_static_weak (__nss_database_fork_prepare_parent,
|
||||
&nss_database_data);
|
||||
|
||||
_IO_list_lock ();
|
||||
|
||||
/* Acquire malloc locks. This needs to come last because fork
|
||||
@ -118,6 +124,9 @@ __libc_fork (void)
|
||||
|
||||
/* Reset locks in the I/O code. */
|
||||
_IO_list_resetlock ();
|
||||
|
||||
call_function_static_weak (__nss_database_fork_subprocess,
|
||||
&nss_database_data);
|
||||
}
|
||||
|
||||
/* Reset the lock the dynamic loader uses to protect its data. */
|
||||
|
Loading…
Reference in New Issue
Block a user