mirror of
https://gitlab.com/apparmor/apparmor
synced 2025-08-30 05:47:59 +00:00
parser: add include dedup cache to handle include loops
Profile includes can be setup to loop and expand in a pathalogical manner that causes build failures. Fix this by caching which includes have already been seen in a given profile context. In addition this can speed up some profile compiles, that end up re-including common abstractions. By not only deduping the files being included but skipping the need to reprocess and dedup the rules within the include. Fixes: https://bugzilla.suse.com/show_bug.cgi?id=1184779 MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/743 Signed-off-by: John Johansen <john.johansen@canonical.com> Acked-by: Steve Beattie <steve.beattie@canonical.com>
This commit is contained in:
parent
a7816e1a8f
commit
7dcf013bca
@ -102,7 +102,7 @@ SRCS = parser_common.c parser_include.c parser_interface.c parser_lex.c \
|
|||||||
af_rule.cc af_unix.cc policy_cache.c default_features.c
|
af_rule.cc af_unix.cc policy_cache.c default_features.c
|
||||||
HDRS = parser.h parser_include.h immunix.h mount.h dbus.h lib.h profile.h \
|
HDRS = parser.h parser_include.h immunix.h mount.h dbus.h lib.h profile.h \
|
||||||
rule.h common_optarg.h signal.h ptrace.h network.h af_rule.h af_unix.h \
|
rule.h common_optarg.h signal.h ptrace.h network.h af_rule.h af_unix.h \
|
||||||
policy_cache.h
|
policy_cache.h file_cache.h
|
||||||
TOOLS = apparmor_parser
|
TOOLS = apparmor_parser
|
||||||
|
|
||||||
OBJECTS = $(patsubst %.cc, %.o, $(SRCS:.c=.o))
|
OBJECTS = $(patsubst %.cc, %.o, $(SRCS:.c=.o))
|
||||||
@ -215,10 +215,10 @@ apparmor_parser: $(OBJECTS) $(AAREOBJECTS) $(LIBAPPARMOR_A)
|
|||||||
$(CXX) $(LDFLAGS) $(EXTRA_CFLAGS) -o $@ $(OBJECTS) $(LIBS) \
|
$(CXX) $(LDFLAGS) $(EXTRA_CFLAGS) -o $@ $(OBJECTS) $(LIBS) \
|
||||||
${LEXLIB} $(AAREOBJECTS) $(AARE_LDFLAGS) $(AALIB)
|
${LEXLIB} $(AAREOBJECTS) $(AARE_LDFLAGS) $(AALIB)
|
||||||
|
|
||||||
parser_yacc.c parser_yacc.h: parser_yacc.y parser.h profile.h
|
parser_yacc.c parser_yacc.h: parser_yacc.y parser.h profile.h file_cache.h
|
||||||
$(YACC) $(YFLAGS) -o parser_yacc.c parser_yacc.y
|
$(YACC) $(YFLAGS) -o parser_yacc.c parser_yacc.y
|
||||||
|
|
||||||
parser_lex.c: parser_lex.l parser_yacc.h parser.h profile.h mount.h dbus.h policy_cache.h
|
parser_lex.c: parser_lex.l parser_yacc.h parser.h profile.h mount.h dbus.h policy_cache.h file_cache.h
|
||||||
$(LEX) ${LEXFLAGS} -o$@ $<
|
$(LEX) ${LEXFLAGS} -o$@ $<
|
||||||
|
|
||||||
parser_lex.o: parser_lex.c parser.h parser_yacc.h
|
parser_lex.o: parser_lex.c parser.h parser_yacc.h
|
||||||
@ -230,13 +230,13 @@ parser_misc.o: parser_misc.c parser.h parser_yacc.h profile.h cap_names.h $(APPA
|
|||||||
parser_yacc.o: parser_yacc.c parser_yacc.h $(APPARMOR_H)
|
parser_yacc.o: parser_yacc.c parser_yacc.h $(APPARMOR_H)
|
||||||
$(CXX) $(EXTRA_CFLAGS) -c -o $@ $<
|
$(CXX) $(EXTRA_CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
parser_main.o: parser_main.c parser.h parser_version.h policy_cache.h libapparmor_re/apparmor_re.h $(APPARMOR_H)
|
parser_main.o: parser_main.c parser.h parser_version.h policy_cache.h file_cache.h libapparmor_re/apparmor_re.h $(APPARMOR_H)
|
||||||
$(CXX) $(EXTRA_CFLAGS) -c -o $@ $<
|
$(CXX) $(EXTRA_CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
parser_interface.o: parser_interface.c parser.h profile.h libapparmor_re/apparmor_re.h
|
parser_interface.o: parser_interface.c parser.h profile.h libapparmor_re/apparmor_re.h
|
||||||
$(CXX) $(EXTRA_CFLAGS) -c -o $@ $<
|
$(CXX) $(EXTRA_CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
parser_include.o: parser_include.c parser.h parser_include.h
|
parser_include.o: parser_include.c parser.h parser_include.h file_cache.h
|
||||||
$(CXX) $(EXTRA_CFLAGS) -c -o $@ $<
|
$(CXX) $(EXTRA_CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
parser_merge.o: parser_merge.c parser.h profile.h
|
parser_merge.o: parser_merge.c parser.h profile.h
|
||||||
@ -257,7 +257,7 @@ parser_policy.o: parser_policy.c parser.h parser_yacc.h profile.h
|
|||||||
parser_alias.o: parser_alias.c parser.h profile.h
|
parser_alias.o: parser_alias.c parser.h profile.h
|
||||||
$(CXX) $(EXTRA_CFLAGS) -c -o $@ $<
|
$(CXX) $(EXTRA_CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
parser_common.o: parser_common.c parser.h
|
parser_common.o: parser_common.c parser.h file_cache.h
|
||||||
$(CXX) $(EXTRA_CFLAGS) -c -o $@ $<
|
$(CXX) $(EXTRA_CFLAGS) -c -o $@ $<
|
||||||
|
|
||||||
mount.o: mount.cc mount.h parser.h immunix.h rule.h
|
mount.o: mount.cc mount.h parser.h immunix.h rule.h
|
||||||
|
52
parser/file_cache.h
Normal file
52
parser/file_cache.h
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021
|
||||||
|
* Canonical, Ltd. (All rights reserved)
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of version 2 of the GNU General Public
|
||||||
|
* License published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This program 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 General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, Canonical Ltd.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __AA_FILE_CACHE_H
|
||||||
|
#define __AA_FILE_CACHE_H
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
/* TODO: have includecache be a frontend for file cache, don't just
|
||||||
|
* store name.
|
||||||
|
*/
|
||||||
|
class IncludeCache_t {
|
||||||
|
public:
|
||||||
|
set<string> cache;
|
||||||
|
|
||||||
|
IncludeCache_t() = default;
|
||||||
|
virtual ~IncludeCache_t() = default;
|
||||||
|
|
||||||
|
/* return true if in set */
|
||||||
|
bool find(const char *name) {
|
||||||
|
return cache.find(name) != cache.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool insert(const char *name) {
|
||||||
|
pair<set<string>::iterator,bool> res = cache.insert(name);
|
||||||
|
if (res.second == false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/* inserted */
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* __AA_FILE_CACHE_H */
|
@ -32,6 +32,7 @@
|
|||||||
|
|
||||||
#include <sys/apparmor.h>
|
#include <sys/apparmor.h>
|
||||||
|
|
||||||
|
#include "file_cache.h"
|
||||||
#include "immunix.h"
|
#include "immunix.h"
|
||||||
#include "libapparmor_re/apparmor_re.h"
|
#include "libapparmor_re/apparmor_re.h"
|
||||||
#include "libapparmor_re/aare_rules.h"
|
#include "libapparmor_re/aare_rules.h"
|
||||||
@ -353,6 +354,8 @@ extern char *profile_ns;
|
|||||||
extern char *current_filename;
|
extern char *current_filename;
|
||||||
extern FILE *ofile;
|
extern FILE *ofile;
|
||||||
extern int read_implies_exec;
|
extern int read_implies_exec;
|
||||||
|
extern IncludeCache_t *g_includecache;
|
||||||
|
|
||||||
extern void pwarnf(bool werr, const char *fmt, ...) __attribute__((__format__(__printf__, 2, 3)));
|
extern void pwarnf(bool werr, const char *fmt, ...) __attribute__((__format__(__printf__, 2, 3)));
|
||||||
extern void common_warn_once(const char *name, const char *msg, const char **warned_name);
|
extern void common_warn_once(const char *name, const char *msg, const char **warned_name);
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
|
|
||||||
#include "parser.h"
|
#include "parser.h"
|
||||||
|
#include "file_cache.h"
|
||||||
|
|
||||||
/* Policy versioning is determined by a combination of 3 values:
|
/* Policy versioning is determined by a combination of 3 values:
|
||||||
* policy_version: version of txt policy
|
* policy_version: version of txt policy
|
||||||
@ -95,6 +96,8 @@ char *current_filename = NULL;
|
|||||||
|
|
||||||
FILE *ofile = NULL;
|
FILE *ofile = NULL;
|
||||||
|
|
||||||
|
IncludeCache_t *g_includecache;
|
||||||
|
|
||||||
#ifdef FORCE_READ_IMPLIES_EXEC
|
#ifdef FORCE_READ_IMPLIES_EXEC
|
||||||
int read_implies_exec = 1;
|
int read_implies_exec = 1;
|
||||||
#else
|
#else
|
||||||
|
@ -151,7 +151,7 @@ void parse_default_paths(void)
|
|||||||
add_search_dir(basedir);
|
add_search_dir(basedir);
|
||||||
}
|
}
|
||||||
|
|
||||||
FILE *search_path(char *filename, char **fullpath)
|
FILE *search_path(char *filename, char **fullpath, bool *skip)
|
||||||
{
|
{
|
||||||
FILE *newf = NULL;
|
FILE *newf = NULL;
|
||||||
char *buf = NULL;
|
char *buf = NULL;
|
||||||
@ -161,15 +161,27 @@ FILE *search_path(char *filename, char **fullpath)
|
|||||||
perror("asprintf");
|
perror("asprintf");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (g_includecache->find(buf)) {
|
||||||
|
/* hit do not want to re-include */
|
||||||
|
*skip = true;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
newf = fopen(buf, "r");
|
newf = fopen(buf, "r");
|
||||||
if (newf && fullpath)
|
if (newf) {
|
||||||
*fullpath = buf;
|
/* ignore failing to insert into cache */
|
||||||
else
|
(void) g_includecache->insert(buf);
|
||||||
free(buf);
|
if (fullpath)
|
||||||
buf = NULL;
|
*fullpath = buf;
|
||||||
if (newf)
|
else
|
||||||
|
free(buf);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
free(buf);
|
||||||
|
buf = NULL;
|
||||||
}
|
}
|
||||||
|
*skip = false;
|
||||||
return newf;
|
return newf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ extern void init_base_dir(void);
|
|||||||
extern void set_base_dir(char *dir);
|
extern void set_base_dir(char *dir);
|
||||||
extern void parse_default_paths(void);
|
extern void parse_default_paths(void);
|
||||||
extern int do_include_preprocessing(char *profilename);
|
extern int do_include_preprocessing(char *profilename);
|
||||||
FILE *search_path(char *filename, char **fullpath);
|
FILE *search_path(char *filename, char **fullpath, bool *skip);
|
||||||
|
|
||||||
extern void push_include_stack(char *filename);
|
extern void push_include_stack(char *filename);
|
||||||
extern void pop_include_stack(void);
|
extern void pop_include_stack(void);
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
#include "parser_yacc.h"
|
#include "parser_yacc.h"
|
||||||
#include "lib.h"
|
#include "lib.h"
|
||||||
#include "policy_cache.h"
|
#include "policy_cache.h"
|
||||||
|
#include "file_cache.h"
|
||||||
|
|
||||||
#ifdef PDEBUG
|
#ifdef PDEBUG
|
||||||
#undef PDEBUG
|
#undef PDEBUG
|
||||||
@ -134,10 +135,17 @@ static int include_dir_cb(int dirfd unused, const char *name, struct stat *st,
|
|||||||
if (is_blacklisted(name, path))
|
if (is_blacklisted(name, path))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
if (g_includecache->find(path)) {
|
||||||
|
PDEBUG("skipping reinclude of \'%s\' in \'%s\'\n", path,
|
||||||
|
d->filename);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (S_ISREG(st->st_mode)) {
|
if (S_ISREG(st->st_mode)) {
|
||||||
if (!(yyin = fopen(path,"r")))
|
if (!(yyin = fopen(path,"r")))
|
||||||
yyerror(_("Could not open '%s' in '%s'"), path, d->filename);
|
yyerror(_("Could not open '%s' in '%s'"), path, d->filename);
|
||||||
PDEBUG("Opened include \"%s\" in \"%s\"\n", path, d->filename);
|
PDEBUG("Opened include \"%s\" in \"%s\"\n", path, d->filename);
|
||||||
|
(void) g_includecache->insert(path);
|
||||||
update_mru_tstamp(yyin, path);
|
update_mru_tstamp(yyin, path);
|
||||||
push_include_stack(path);
|
push_include_stack(path);
|
||||||
yypush_buffer_state(yy_create_buffer(yyin, YY_BUF_SIZE));
|
yypush_buffer_state(yy_create_buffer(yyin, YY_BUF_SIZE));
|
||||||
@ -151,16 +159,29 @@ void include_filename(char *filename, int search, bool if_exists)
|
|||||||
FILE *include_file = NULL;
|
FILE *include_file = NULL;
|
||||||
struct stat my_stat;
|
struct stat my_stat;
|
||||||
autofree char *fullpath = NULL;
|
autofree char *fullpath = NULL;
|
||||||
|
bool cached;
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
if (preprocess_only)
|
include_file = search_path(filename, &fullpath, &cached);
|
||||||
|
if (!include_file && cached) {
|
||||||
|
goto skip;
|
||||||
|
} else if (preprocess_only) {
|
||||||
fprintf(yyout, "\n\n##included <%s>\n", filename);
|
fprintf(yyout, "\n\n##included <%s>\n", filename);
|
||||||
include_file = search_path(filename, &fullpath);
|
} else if (!include_file && preprocess_only) {
|
||||||
|
fprintf(yyout, "\n\n##failed include <%s>\n", filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (g_includecache->find(filename)) {
|
||||||
|
/* duplicate entry skip */
|
||||||
|
goto skip;
|
||||||
} else {
|
} else {
|
||||||
if (preprocess_only)
|
if (preprocess_only)
|
||||||
fprintf(yyout, "\n\n##included \"%s\"\n", filename);
|
fprintf(yyout, "\n\n##included \"%s\"\n", filename);
|
||||||
fullpath = strdup(filename);
|
fullpath = strdup(filename);
|
||||||
include_file = fopen(fullpath, "r");
|
include_file = fopen(fullpath, "r");
|
||||||
|
if (include_file)
|
||||||
|
/* ignore failure to insert into cache */
|
||||||
|
(void) g_includecache->insert(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!include_file) {
|
if (!include_file) {
|
||||||
@ -188,6 +209,13 @@ void include_filename(char *filename, int search, bool if_exists)
|
|||||||
" '%s' in '%s'"), fullpath, filename);;
|
" '%s' in '%s'"), fullpath, filename);;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
skip:
|
||||||
|
if (preprocess_only)
|
||||||
|
fprintf(yyout, "\n\n##skipped duplicate include <%s>\n", filename);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char *lsntrim(char *s, int l)
|
static char *lsntrim(char *s, int l)
|
||||||
|
@ -50,6 +50,7 @@
|
|||||||
#include "common_optarg.h"
|
#include "common_optarg.h"
|
||||||
#include "policy_cache.h"
|
#include "policy_cache.h"
|
||||||
#include "libapparmor_re/apparmor_re.h"
|
#include "libapparmor_re/apparmor_re.h"
|
||||||
|
#include "file_cache.h"
|
||||||
|
|
||||||
#define OLD_MODULE_NAME "subdomain"
|
#define OLD_MODULE_NAME "subdomain"
|
||||||
#define PROC_MODULES "/proc/modules"
|
#define PROC_MODULES "/proc/modules"
|
||||||
@ -1035,6 +1036,8 @@ void reset_parser(const char *filename)
|
|||||||
aa_features_unref(policy_features);
|
aa_features_unref(policy_features);
|
||||||
policy_features = NULL;
|
policy_features = NULL;
|
||||||
clear_cap_flag(CAPFLAG_POLICY_FEATURE);
|
clear_cap_flag(CAPFLAG_POLICY_FEATURE);
|
||||||
|
delete g_includecache;
|
||||||
|
g_includecache = new IncludeCache_t();
|
||||||
}
|
}
|
||||||
|
|
||||||
int test_for_dir_mode(const char *basename, const char *linkdir)
|
int test_for_dir_mode(const char *basename, const char *linkdir)
|
||||||
|
@ -220,6 +220,7 @@ void add_local_entry(Profile *prof);
|
|||||||
struct cond_entry_list cond_entry_list;
|
struct cond_entry_list cond_entry_list;
|
||||||
int boolean;
|
int boolean;
|
||||||
struct prefixes prefix;
|
struct prefixes prefix;
|
||||||
|
IncludeCache_t *includecache;
|
||||||
}
|
}
|
||||||
|
|
||||||
%type <id> TOK_ID
|
%type <id> TOK_ID
|
||||||
@ -334,9 +335,17 @@ opt_id: { /* nothing */ $$ = NULL; }
|
|||||||
opt_id_or_var: { /* nothing */ $$ = NULL; }
|
opt_id_or_var: { /* nothing */ $$ = NULL; }
|
||||||
| id_or_var { $$ = $1; }
|
| id_or_var { $$ = $1; }
|
||||||
|
|
||||||
profile_base: TOK_ID opt_id_or_var opt_cond_list flags TOK_OPEN rules TOK_CLOSE
|
profile_base: TOK_ID opt_id_or_var opt_cond_list flags TOK_OPEN
|
||||||
{
|
{
|
||||||
Profile *prof = $6;
|
/* mid rule action
|
||||||
|
* save current cache, restore at end of block
|
||||||
|
*/
|
||||||
|
$<includecache>$ = g_includecache;
|
||||||
|
g_includecache = new IncludeCache_t();
|
||||||
|
}
|
||||||
|
rules TOK_CLOSE
|
||||||
|
{
|
||||||
|
Profile *prof = $7;
|
||||||
bool self_stack = false;
|
bool self_stack = false;
|
||||||
|
|
||||||
if (!prof) {
|
if (!prof) {
|
||||||
@ -387,6 +396,10 @@ profile_base: TOK_ID opt_id_or_var opt_cond_list flags TOK_OPEN rules TOK_CLOSE
|
|||||||
post_process_file_entries(prof);
|
post_process_file_entries(prof);
|
||||||
post_process_rule_entries(prof);
|
post_process_rule_entries(prof);
|
||||||
prof->flags.debug(cerr);
|
prof->flags.debug(cerr);
|
||||||
|
|
||||||
|
/* restore previous blocks include cache */
|
||||||
|
delete g_includecache;
|
||||||
|
g_includecache = $<includecache>6;
|
||||||
$$ = prof;
|
$$ = prof;
|
||||||
|
|
||||||
};
|
};
|
||||||
@ -1775,12 +1788,17 @@ static int abi_features_base(struct aa_features **features, char *filename, bool
|
|||||||
autofclose FILE *f = NULL;
|
autofclose FILE *f = NULL;
|
||||||
struct stat my_stat;
|
struct stat my_stat;
|
||||||
char *fullpath = NULL;
|
char *fullpath = NULL;
|
||||||
|
bool cached;
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
if (strcmp(filename, "kernel") == 0)
|
if (strcmp(filename, "kernel") == 0)
|
||||||
return aa_features_new_from_kernel(features);
|
return aa_features_new_from_kernel(features);
|
||||||
f = search_path(filename, &fullpath);
|
f = search_path(filename, &fullpath, &cached);
|
||||||
PDEBUG("abi lookup '%s' -> '%s' f %p\n", filename, fullpath, f);
|
PDEBUG("abi lookup '%s' -> '%s' f %p cached %d\n", filename, fullpath, f, cached);
|
||||||
|
if (!f && cached) {
|
||||||
|
*features = NULL;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
f = fopen(filename, "r");
|
f = fopen(filename, "r");
|
||||||
PDEBUG("abi relpath '%s' f %p\n", filename, f);
|
PDEBUG("abi relpath '%s' f %p\n", filename, f);
|
||||||
@ -1809,10 +1827,15 @@ static void abi_features(char *filename, bool search)
|
|||||||
yyerror(_("failed to find features abi '%s': %m"), filename);
|
yyerror(_("failed to find features abi '%s': %m"), filename);
|
||||||
}
|
}
|
||||||
if (policy_features) {
|
if (policy_features) {
|
||||||
if (!aa_features_is_equal(tmp_features, policy_features)) {
|
if (tmp_features) {
|
||||||
pwarn(WARN_ABI, _("%s: %s features abi '%s' differs from policy declared feature abi, using the features abi declared in policy\n"), progname, current_filename, filename);
|
if (!aa_features_is_equal(tmp_features, policy_features)) {
|
||||||
|
pwarn(WARN_ABI, _("%s: %s features abi '%s' differs from policy declared feature abi, using the features abi declared in policy\n"), progname, current_filename, filename);
|
||||||
|
}
|
||||||
|
aa_features_unref(tmp_features);
|
||||||
}
|
}
|
||||||
aa_features_unref(tmp_features);
|
} else if (!tmp_features) {
|
||||||
|
/* skipped reinclude, but features not set */
|
||||||
|
yyerror(_("failed features abi not set but include cache skipped\n"));
|
||||||
} else {
|
} else {
|
||||||
/* first features abi declaration */
|
/* first features abi declaration */
|
||||||
policy_features = tmp_features;
|
policy_features = tmp_features;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user