mirror of
https://github.com/darlinghq/darling.git
synced 2024-11-27 14:20:24 +00:00
Working threading via libelfloader, got rid of the reaper thread
This commit is contained in:
parent
0d9e0bc82b
commit
e5b19f0da3
@ -6,7 +6,7 @@ function(wrap_elf name elfname)
|
||||
OUTPUT
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${name}.c
|
||||
COMMAND
|
||||
${CMAKE_BINARY_DIR}/src/libelfloader/wrapgen
|
||||
${CMAKE_BINARY_DIR}/src/libelfloader/wrapgen/wrapgen
|
||||
${elfname}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${name}.c
|
||||
DEPENDS
|
||||
|
@ -9,6 +9,7 @@
|
||||
#include "../../../../../platform-include/sys/errno.h"
|
||||
#include "../mman/mman.h"
|
||||
#include "../simple.h"
|
||||
#include "../elfcalls_wrapper.h"
|
||||
|
||||
extern void *memset(void *s, int c, size_t n);
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
#include <linux-syscalls/linux.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "../elfcalls_wrapper.h"
|
||||
|
||||
int bsdthread_terminate_trap(
|
||||
uintptr_t stackaddr,
|
||||
|
@ -1,45 +1,68 @@
|
||||
#include "elfcalls_wrapper.h"
|
||||
#include <elfcalls.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
extern struct elf_calls* _elfcalls;
|
||||
static struct elf_calls* _elfcalls;
|
||||
|
||||
struct elf_calls* elfcalls(void)
|
||||
{
|
||||
if (!_elfcalls)
|
||||
{
|
||||
void* module = dlopen("/usr/lib/libelfloader.dylib", RTLD_NOW);
|
||||
// if (!module)
|
||||
// __simple_printf("Load error: %s\n", dlerror());
|
||||
|
||||
// struct elf_calls** ptr = (struct elf_calls**) dlsym(module, "_elfcalls");
|
||||
// __simple_printf("_elfcalls is at %p\n", ptr);
|
||||
// __simple_printf("*_elfcalls = %p\n", *ptr);
|
||||
_elfcalls = *(struct elf_calls**) dlsym(module, "_elfcalls");
|
||||
}
|
||||
return _elfcalls;
|
||||
}
|
||||
|
||||
void native_exit(int ec)
|
||||
{
|
||||
if (_elfcalls)
|
||||
_elfcalls->exit(ec);
|
||||
}
|
||||
|
||||
void* __darling_thread_create(unsigned long stack_size, unsigned long pthobj_size,
|
||||
void* entry_point, uintptr_t arg3,
|
||||
uintptr_t arg4, uintptr_t arg5, uintptr_t arg6,
|
||||
int (*thread_self_trap)())
|
||||
{
|
||||
return _elfcalls->darling_thread_create(stack_size, pthobj_size, entry_point,
|
||||
return elfcalls()->darling_thread_create(stack_size, pthobj_size, entry_point,
|
||||
arg3, arg4, arg5, arg6, thread_self_trap);
|
||||
}
|
||||
|
||||
int __darling_thread_terminate(void* stackaddr,
|
||||
unsigned long freesize, unsigned long pthobj_size)
|
||||
{
|
||||
return _elfcalls->darling_thread_terminate(stackaddr, freesize, pthobj_size);
|
||||
return elfcalls()->darling_thread_terminate(stackaddr, freesize, pthobj_size);
|
||||
}
|
||||
|
||||
void* __darling_thread_get_stack(void)
|
||||
{
|
||||
return _elfcalls->darling_thread_get_stack();
|
||||
return elfcalls()->darling_thread_get_stack();
|
||||
}
|
||||
|
||||
void* native_dlopen(const char* path)
|
||||
{
|
||||
return _elfcalls->dlopen(path);
|
||||
return elfcalls()->dlopen(path);
|
||||
}
|
||||
|
||||
char* native_dlerror(void)
|
||||
{
|
||||
return _elfcalls->dlerror();
|
||||
return elfcalls()->dlerror();
|
||||
}
|
||||
|
||||
void* native_dlsym(void* module, const char* name)
|
||||
{
|
||||
return _elfcalls->dlsym(module, name);
|
||||
return elfcalls()->dlsym(module, name);
|
||||
}
|
||||
|
||||
int native_dlclose(void* module)
|
||||
{
|
||||
return _elfcalls->dlclose(module);
|
||||
return elfcalls()->dlclose(module);
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,8 @@ char* native_dlerror(void);
|
||||
void* native_dlsym(void* module, const char* name);
|
||||
int native_dlclose(void* module);
|
||||
|
||||
void native_exit(int ec);
|
||||
|
||||
// Native thread wrapping
|
||||
void* __darling_thread_create(unsigned long stack_size, unsigned long pthobj_size,
|
||||
void* entry_point, uintptr_t arg3,
|
||||
|
@ -2,11 +2,14 @@
|
||||
#include "../base.h"
|
||||
#include "../errno.h"
|
||||
#include <linux-syscalls/linux.h>
|
||||
#include "../elfcalls_wrapper.h"
|
||||
|
||||
long sys_exit(int status)
|
||||
{
|
||||
int ret;
|
||||
|
||||
native_exit(status);
|
||||
|
||||
ret = LINUX_SYSCALL1(__NR_exit_group, status);
|
||||
if (ret < 0)
|
||||
ret = errno_linux_to_bsd(ret);
|
||||
|
@ -5,16 +5,16 @@
|
||||
#include <linux-syscalls/linux.h>
|
||||
#include <elfcalls.h>
|
||||
|
||||
extern struct elf_calls* _elfcalls;
|
||||
extern struct elf_calls* elfcalls(void);
|
||||
|
||||
long sys_sem_close(int sem)
|
||||
{
|
||||
#ifndef VARIANT_DYLD
|
||||
int ret;
|
||||
|
||||
ret = _elfcalls->sem_close(sem);
|
||||
ret = elfcalls()->sem_close(sem);
|
||||
if (ret == -1)
|
||||
ret = -errno_linux_to_bsd(_elfcalls->get_errno());
|
||||
ret = -errno_linux_to_bsd(elfcalls()->get_errno());
|
||||
|
||||
return ret;
|
||||
#else
|
||||
|
@ -6,7 +6,7 @@
|
||||
#include <linux-syscalls/linux.h>
|
||||
#include <elfcalls.h>
|
||||
|
||||
extern struct elf_calls* _elfcalls;
|
||||
extern struct elf_calls* elfcalls(void);
|
||||
|
||||
long sys_sem_open(const char* name, int oflag, int mode, int value)
|
||||
{
|
||||
@ -16,13 +16,13 @@ long sys_sem_open(const char* name, int oflag, int mode, int value)
|
||||
|
||||
// __simple_printf("sem_open %s, %d, %d, %d\n", name, oflag, mode, value);
|
||||
|
||||
ptr = _elfcalls->sem_open(name, oflags_bsd_to_linux(oflag), mode, value);
|
||||
ptr = elfcalls()->sem_open(name, oflags_bsd_to_linux(oflag), mode, value);
|
||||
//__simple_printf("sem_open -> %p\n", ptr);
|
||||
|
||||
if (!ptr)
|
||||
{
|
||||
// __simple_printf("errno: %d\n", _elfcalls->get_errno());
|
||||
return -errno_linux_to_bsd(_elfcalls->get_errno());
|
||||
// __simple_printf("errno: %d\n", elfcalls()->get_errno());
|
||||
return -errno_linux_to_bsd(elfcalls()->get_errno());
|
||||
}
|
||||
|
||||
return (long) ptr;
|
||||
|
@ -5,7 +5,7 @@
|
||||
#include <linux-syscalls/linux.h>
|
||||
#include <elfcalls.h>
|
||||
|
||||
extern struct elf_calls* _elfcalls;
|
||||
extern struct elf_calls* elfcalls(void);
|
||||
|
||||
long sys_sem_post(int* sem)
|
||||
{
|
||||
@ -13,9 +13,9 @@ long sys_sem_post(int* sem)
|
||||
int ret;
|
||||
|
||||
// __simple_printf("sem_post(%p)\n", sem);
|
||||
ret = _elfcalls->sem_post(sem);
|
||||
ret = elfcalls()->sem_post(sem);
|
||||
if (ret == -1)
|
||||
ret = -errno_linux_to_bsd(_elfcalls->get_errno());
|
||||
ret = -errno_linux_to_bsd(elfcalls()->get_errno());
|
||||
|
||||
return ret;
|
||||
#else
|
||||
|
@ -5,16 +5,16 @@
|
||||
#include <linux-syscalls/linux.h>
|
||||
#include <elfcalls.h>
|
||||
|
||||
extern struct elf_calls* _elfcalls;
|
||||
extern struct elf_calls* elfcalls(void);
|
||||
|
||||
long sys_sem_trywait(int* sem)
|
||||
{
|
||||
#ifndef VARIANT_DYLD
|
||||
int ret;
|
||||
|
||||
ret = _elfcalls->sem_trywait(sem);
|
||||
ret = elfcalls()->sem_trywait(sem);
|
||||
if (ret == -1)
|
||||
ret = -errno_linux_to_bsd(_elfcalls->get_errno());
|
||||
ret = -errno_linux_to_bsd(elfcalls()->get_errno());
|
||||
|
||||
return ret;
|
||||
#else
|
||||
|
@ -5,16 +5,16 @@
|
||||
#include <linux-syscalls/linux.h>
|
||||
#include <elfcalls.h>
|
||||
|
||||
extern struct elf_calls* _elfcalls;
|
||||
extern struct elf_calls* elfcalls(void);
|
||||
|
||||
long sys_sem_unlink(const char* name)
|
||||
{
|
||||
#ifndef VARIANT_DYLD
|
||||
int ret;
|
||||
|
||||
ret = _elfcalls->sem_unlink(name);
|
||||
ret = elfcalls()->sem_unlink(name);
|
||||
if (ret == -1)
|
||||
ret = -errno_linux_to_bsd(_elfcalls->get_errno());
|
||||
ret = -errno_linux_to_bsd(elfcalls()->get_errno());
|
||||
|
||||
return ret;
|
||||
#else
|
||||
|
@ -6,7 +6,7 @@
|
||||
#include <elfcalls.h>
|
||||
#include "../bsdthread/cancelable.h"
|
||||
|
||||
extern struct elf_calls* _elfcalls;
|
||||
extern struct elf_calls* elfcalls(void);
|
||||
|
||||
long sys_sem_wait(int* sem)
|
||||
{
|
||||
@ -19,9 +19,9 @@ long sys_sem_wait_nocancel(int* sem)
|
||||
#ifndef VARIANT_DYLD
|
||||
int ret;
|
||||
|
||||
ret = _elfcalls->sem_wait(sem);
|
||||
ret = elfcalls()->sem_wait(sem);
|
||||
if (ret == -1)
|
||||
ret = -errno_linux_to_bsd(_elfcalls->get_errno());
|
||||
ret = -errno_linux_to_bsd(elfcalls()->get_errno());
|
||||
|
||||
return ret;
|
||||
#else
|
||||
|
@ -6,16 +6,16 @@
|
||||
#include <linux-syscalls/linux.h>
|
||||
#include <elfcalls.h>
|
||||
|
||||
extern struct elf_calls* _elfcalls;
|
||||
extern struct elf_calls* elfcalls(void);
|
||||
|
||||
long sys_shm_open(const char* name, int oflag, int mode)
|
||||
{
|
||||
#ifndef VARIANT_DYLD
|
||||
int ret;
|
||||
|
||||
ret = _elfcalls->shm_open(name, oflags_bsd_to_linux(oflag), mode);
|
||||
ret = elfcalls()->shm_open(name, oflags_bsd_to_linux(oflag), mode);
|
||||
if (ret == -1)
|
||||
ret = -errno_linux_to_bsd(_elfcalls->get_errno());
|
||||
ret = -errno_linux_to_bsd(elfcalls()->get_errno());
|
||||
|
||||
return ret;
|
||||
#else
|
||||
|
@ -5,16 +5,16 @@
|
||||
#include <linux-syscalls/linux.h>
|
||||
#include <elfcalls.h>
|
||||
|
||||
extern struct elf_calls* _elfcalls;
|
||||
extern struct elf_calls* elfcalls(void);
|
||||
|
||||
long sys_shm_unlink(const char* name)
|
||||
{
|
||||
#ifndef VARIANT_DYLD
|
||||
int ret;
|
||||
|
||||
ret = _elfcalls->shm_unlink(name);
|
||||
ret = elfcalls()->shm_unlink(name);
|
||||
if (ret == -1)
|
||||
ret = -errno_linux_to_bsd(_elfcalls->get_errno());
|
||||
ret = -errno_linux_to_bsd(elfcalls()->get_errno());
|
||||
|
||||
return ret;
|
||||
#else
|
||||
|
@ -179,7 +179,7 @@ void run(const char* path)
|
||||
JUMPX(stack, lc.interp_entry);
|
||||
else
|
||||
{
|
||||
puts("Back from loaded binary");
|
||||
// puts("Back from loaded binary");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,6 +65,7 @@ void elfcalls_make(struct elf_calls* calls)
|
||||
calls->darling_thread_get_stack = __darling_thread_get_stack;
|
||||
|
||||
calls->get_errno = get_errno;
|
||||
calls->exit = exit;
|
||||
*((void**)&calls->sem_open) = sem_open;
|
||||
*((void**)&calls->sem_wait) = sem_wait;
|
||||
*((void**)&calls->sem_trywait) = sem_trywait;
|
||||
@ -80,20 +81,22 @@ int main(int argc, const char** argv)
|
||||
{
|
||||
typedef void (*retfunc)(void);
|
||||
|
||||
pthread_once(&once_control, once_test);
|
||||
|
||||
struct elf_calls* calls;
|
||||
retfunc ret;
|
||||
|
||||
for (int i = 0; i < argc; i++)
|
||||
printf("arg %d: %s\n", i, argv[i]);
|
||||
// for (int i = 0; i < argc; i++)
|
||||
// printf("arg %d: %s\n", i, argv[i]);
|
||||
|
||||
calls = (struct elf_calls*) strtoul(argv[1], NULL, 16);
|
||||
ret = (retfunc) strtoul(argv[2], NULL, 16);
|
||||
|
||||
puts("before elfcalls_make");
|
||||
// puts("before elfcalls_make");
|
||||
|
||||
elfcalls_make(calls);
|
||||
puts("after elfcalls_make");
|
||||
printf("Will call %p\n", ret);
|
||||
// puts("after elfcalls_make");
|
||||
// printf("Will call %p\n", ret);
|
||||
ret();
|
||||
|
||||
__builtin_unreachable();
|
||||
|
@ -36,6 +36,8 @@ struct elf_calls
|
||||
// POSIX SHM APIs
|
||||
int (*shm_open)(const char* name, int oflag, unsigned short mode);
|
||||
int (*shm_unlink)(const char* name);
|
||||
|
||||
void (*exit)(int ec);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
This file is part of Darling.
|
||||
|
||||
Copyright (C) 2015 Lubos Dolezel
|
||||
Copyright (C) 2015-2018 Lubos Dolezel
|
||||
|
||||
Darling is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@ -27,6 +27,7 @@ along with Darling. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <setjmp.h>
|
||||
|
||||
// The point of this file is build macOS threads on top of native libc's threads,
|
||||
// otherwise it would not be possible to make native calls from these threads.
|
||||
@ -46,23 +47,10 @@ struct arg_struct
|
||||
};
|
||||
unsigned long pth_obj_size;
|
||||
void* pth;
|
||||
};
|
||||
struct reaper_item
|
||||
{
|
||||
struct reaper_item* next;
|
||||
pthread_t thread;
|
||||
void* stack;
|
||||
size_t stacksize;
|
||||
jmp_buf* jmpbuf;
|
||||
};
|
||||
|
||||
static void* darling_thread_entry(void* p);
|
||||
static void start_reaper();
|
||||
|
||||
static sem_t reaper_sem;
|
||||
static pthread_mutex_t reaper_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static struct reaper_item *reaper_items_front = NULL, *reaper_items_end = NULL;
|
||||
static void reaper_item_push(struct reaper_item* item);
|
||||
static struct reaper_item* reaper_item_pop(void);
|
||||
|
||||
#ifndef PTHREAD_STACK_MIN
|
||||
# define PTHREAD_STACK_MIN 16384
|
||||
@ -73,21 +61,17 @@ void* __darling_thread_create(unsigned long stack_size, unsigned long pth_obj_si
|
||||
uintptr_t arg4, uintptr_t arg5, uintptr_t arg6,
|
||||
int (*thread_self_trap)())
|
||||
{
|
||||
static pthread_once_t reaper_once = PTHREAD_ONCE_INIT;
|
||||
|
||||
struct arg_struct args = { (thread_ep) entry_point, arg3,
|
||||
arg4, arg5, arg6, thread_self_trap, pth_obj_size, NULL };
|
||||
pthread_attr_t attr;
|
||||
pthread_t nativeLibcThread;
|
||||
void* pth;
|
||||
|
||||
pthread_once(&reaper_once, start_reaper);
|
||||
|
||||
pthread_attr_init(&attr);
|
||||
//pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||||
// pthread_attr_setstacksize(&attr, stack_size);
|
||||
|
||||
pth = mmap(NULL, stack_size + pth_obj_size + 0x1000, PROT_READ | PROT_WRITE,
|
||||
pth = mmap(NULL, stack_size + pth_obj_size + 0x1000 + 0x1000, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
|
||||
// pthread_attr_setstack is buggy. The documentation states we should provide the lowest
|
||||
@ -97,7 +81,10 @@ void* __darling_thread_create(unsigned long stack_size, unsigned long pth_obj_si
|
||||
//pthread_attr_setstack(&attr, ((char*)pth) + pth_obj_size, stack_size - pth_obj_size - 0x1000);
|
||||
|
||||
// std::cout << "Allocated stack at " << pth << ", size " << stack_size << std::endl;
|
||||
pth = ((char*) pth) + stack_size + 0x1000;
|
||||
|
||||
// We allocated an extra page for jmpbuf
|
||||
args.jmpbuf = (jmp_buf*) pth;
|
||||
pth = ((char*) pth) + stack_size + 0x2000;
|
||||
pthread_attr_setstacksize(&attr, 4096);
|
||||
|
||||
args.pth = pth;
|
||||
@ -120,6 +107,15 @@ static void* darling_thread_entry(void* p)
|
||||
args.port = args.thread_self_trap();
|
||||
in_args->pth = NULL;
|
||||
|
||||
int freesize;
|
||||
if ((freesize = setjmp(*args.jmpbuf)) != 0)
|
||||
{
|
||||
// Terminate the Linux thread
|
||||
// +0x1000 is an extra page we allocated for the jmp_buf
|
||||
munmap(args.jmpbuf, freesize + 0x1000);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#ifdef __x86_64__
|
||||
__asm__ __volatile__ (
|
||||
"movq %1, %%rdi\n"
|
||||
@ -159,7 +155,7 @@ static void* darling_thread_entry(void* p)
|
||||
"ret\n" // Jump to the address pushed at the beginning
|
||||
:: "c" (&args), "d" (args.pth));
|
||||
#endif
|
||||
return NULL;
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
int __darling_thread_terminate(void* stackaddr,
|
||||
@ -168,7 +164,7 @@ int __darling_thread_terminate(void* stackaddr,
|
||||
if (getpid() == syscall(SYS_gettid))
|
||||
{
|
||||
// dispatch_main() calls pthread_exit(NULL) on the main thread,
|
||||
// which turns the our process into a zombie.
|
||||
// which turns our process into a zombie on Linux.
|
||||
// Let's just hang around forever.
|
||||
sigset_t mask;
|
||||
memset(&mask, 0, sizeof(mask));
|
||||
@ -176,16 +172,10 @@ int __darling_thread_terminate(void* stackaddr,
|
||||
while (1)
|
||||
sigsuspend(&mask);
|
||||
}
|
||||
|
||||
struct reaper_item* item = (struct reaper_item*) malloc(sizeof(struct reaper_item));
|
||||
item->thread = pthread_self();
|
||||
item->stack = stackaddr;
|
||||
item->stacksize = freesize;
|
||||
reaper_item_push(item);
|
||||
|
||||
sem_post(&reaper_sem);
|
||||
|
||||
pthread_exit(NULL);
|
||||
// Jump back into darling_thread_entry()
|
||||
jmp_buf* jmpbuf = (jmp_buf*) (((char*) stackaddr) - 0x1000);
|
||||
longjmp(*jmpbuf, freesize);
|
||||
|
||||
__builtin_unreachable();
|
||||
}
|
||||
@ -201,81 +191,3 @@ void* __darling_thread_get_stack(void)
|
||||
|
||||
return ((char*)stackaddr) + stacksize - 0x2000;
|
||||
}
|
||||
|
||||
static void* reaper_entry(void* unused)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
struct reaper_item* item;
|
||||
|
||||
sem_wait(&reaper_sem);
|
||||
|
||||
item = reaper_item_pop();
|
||||
if (!item)
|
||||
continue; // Should not happen!
|
||||
|
||||
// std::cout << "Reaping thread " << (void*)item.thread << "; Free stack at " << item.stack << ", " << item.stacksize << " bytes\n";
|
||||
|
||||
// Wait for thread to terminate
|
||||
pthread_join(item->thread, NULL);
|
||||
|
||||
// Free its stack in the extended range requested by Darwin's libc
|
||||
munmap(item->stack, item->stacksize);
|
||||
|
||||
free(item);
|
||||
}
|
||||
}
|
||||
|
||||
static void start_reaper()
|
||||
{
|
||||
pthread_attr_t attr;
|
||||
pthread_t thread;
|
||||
|
||||
pthread_attr_init(&attr);
|
||||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||||
|
||||
sem_init(&reaper_sem, 0, 0);
|
||||
pthread_create(&thread, &attr, reaper_entry, NULL);
|
||||
pthread_attr_destroy(&attr);
|
||||
}
|
||||
|
||||
static void reaper_item_push(struct reaper_item* item)
|
||||
{
|
||||
pthread_mutex_lock(&reaper_mutex);
|
||||
|
||||
item->next = NULL;
|
||||
if (reaper_items_end != NULL)
|
||||
{
|
||||
reaper_items_end->next = item;
|
||||
reaper_items_end = item;
|
||||
}
|
||||
else
|
||||
{
|
||||
reaper_items_front = reaper_items_end = item;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&reaper_mutex);
|
||||
}
|
||||
|
||||
static struct reaper_item* reaper_item_pop(void)
|
||||
{
|
||||
struct reaper_item* e;
|
||||
pthread_mutex_lock(&reaper_mutex);
|
||||
|
||||
if (reaper_items_front != NULL)
|
||||
{
|
||||
e = reaper_items_front;
|
||||
|
||||
if (reaper_items_front == reaper_items_end)
|
||||
reaper_items_front = reaper_items_end = NULL; // The list is now empty
|
||||
else
|
||||
reaper_items_front = e->next;
|
||||
}
|
||||
else
|
||||
e = NULL;
|
||||
|
||||
pthread_mutex_unlock(&reaper_mutex);
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
|
2
src/lkm
2
src/lkm
@ -1 +1 @@
|
||||
Subproject commit dbede9ecaa789b8a8b869d8e2f38deb0d6262140
|
||||
Subproject commit 56f0a2d84783a26ef27e8dde0ba75f4d802be8cc
|
Loading…
Reference in New Issue
Block a user