Unverified Commit 0ef3242e by Patrick Steinhardt Committed by GitHub

Merge pull request #4576 from pks-t/pks/memory-allocator

Custom memory allocators
parents 422cd59b 0f6348f4
......@@ -13,6 +13,9 @@ v0.27 + 1
### API additions
* You can now swap out memory allocators via the
`GIT_OPT_SET_ALLOCATOR` option with `git_libgit2_opts()`.
### API removals
### Breaking API changes
......
......@@ -183,6 +183,7 @@ typedef enum {
GIT_OPT_GET_WINDOWS_SHAREMODE,
GIT_OPT_SET_WINDOWS_SHAREMODE,
GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION,
GIT_OPT_SET_ALLOCATOR
} git_libgit2_opt_t;
/**
......@@ -345,6 +346,12 @@ typedef enum {
* > additional checksum calculation on each object. This defaults
* > to enabled.
*
* opts(GIT_OPT_SET_ALLOCATOR, git_allocator *allocator)
*
* > Set the memory allocator to a different memory allocator. This
* > allocator will then be used to make all memory allocations for
* > libgit2 operations.
*
* @param option Option key
* @param ... value to set the option
* @return 0 on success, <0 on failure
......
/*
* Copyright (C) the libgit2 contributors. All rights reserved.
*
* This file is part of libgit2, distributed under the GNU GPL v2 with
* a Linking Exception. For full terms see the included COPYING file.
*/
#ifndef INCLUDE_sys_git_alloc_h__
#define INCLUDE_sys_git_alloc_h__
#include "git2/common.h"
GIT_BEGIN_DECL
/**
* An instance for a custom memory allocator
*
* Setting the pointers of this structure allows the developer to implement
* custom memory allocators. The global memory allocator can be set by using
* "GIT_OPT_SET_ALLOCATOR" with the `git_libgit2_opts` function. Keep in mind
* that all fields need to be set to a proper function.
*/
typedef struct {
/* Allocate `n` bytes of memory */
void *(*gmalloc)(size_t n, const char *file, int line);
/*
* Allocate memory for an array of `nelem` elements, where each element
* has a size of `elsize`. Returned memory shall be initialized to
* all-zeroes
*/
void *(*gcalloc)(size_t nelem, size_t elsize, const char *file, int line);
/* Allocate memory for the string `str` and duplicate its contents. */
char *(*gstrdup)(const char *str, const char *file, int line);
/*
* Equivalent to the `gstrdup` function, but only duplicating at most
* `n + 1` bytes
*/
char *(*gstrndup)(const char *str, size_t n, const char *file, int line);
/*
* Equivalent to `gstrndup`, but will always duplicate exactly `n` bytes
* of `str`. Thus, out of bounds reads at `str` may happen.
*/
char *(*gsubstrdup)(const char *str, size_t n, const char *file, int line);
/*
* This function shall deallocate the old object `ptr` and return a
* pointer to a new object that has the size specified by `size`. In
* case `ptr` is `NULL`, a new array shall be allocated.
*/
void *(*grealloc)(void *ptr, size_t size, const char *file, int line);
/*
* This function shall be equivalent to `grealloc`, but allocating
* `neleme * elsize` bytes.
*/
void *(*greallocarray)(void *ptr, size_t nelem, size_t elsize, const char *file, int line);
/*
* This function shall allocate a new array of `nelem` elements, where
* each element has a size of `elsize` bytes.
*/
void *(*gmallocarray)(size_t nelem, size_t elsize, const char *file, int line);
/*
* This function shall free the memory pointed to by `ptr`. In case
* `ptr` is `NULL`, this shall be a no-op.
*/
void (*gfree)(void *ptr);
} git_allocator;
/**
* Initialize the allocator structure to use the `stdalloc` pointer.
*
* Set up the structure so that all of its members are using the standard
* "stdalloc" allocator functions. The structure can then be used with
* `git_allocator_setup`.
*
* @param allocator The allocator that is to be initialized.
* @return An error code or 0.
*/
int git_stdalloc_init_allocator(git_allocator *allocator);
/**
* Initialize the allocator structure to use the `crtdbg` pointer.
*
* Set up the structure so that all of its members are using the "crtdbg"
* allocator functions. Note that this allocator is only available on Windows
* platforms and only if libgit2 is being compiled with "-DMSVC_CRTDBG".
*
* @param allocator The allocator that is to be initialized.
* @return An error code or 0.
*/
int git_win32_crtdbg_init_allocator(git_allocator *allocator);
GIT_END_DECL
#endif
/*
* Copyright (C) the libgit2 contributors. All rights reserved.
*
* This file is part of libgit2, distributed under the GNU GPL v2 with
* a Linking Exception. For full terms see the included COPYING file.
*/
#include "alloc.h"
#if defined(GIT_MSVC_CRTDBG)
# include "win32/w32_crtdbg_stacktrace.h"
#else
# include "stdalloc.h"
#endif
git_allocator git__allocator;
int git_allocator_global_init(void)
{
#if defined(GIT_MSVC_CRTDBG)
return git_win32_crtdbg_init_allocator(&git__allocator);
#else
return git_stdalloc_init_allocator(&git__allocator);
#endif
}
int git_allocator_setup(git_allocator *allocator)
{
memcpy(&git__allocator, allocator, sizeof(*allocator));
return 0;
}
#if !defined(GIT_MSVC_CRTDBG)
int git_win32_crtdbg_init_allocator(git_allocator *allocator)
{
GIT_UNUSED(allocator);
giterr_set(GIT_EINVALID, "crtdbg memory allocator not available");
return -1;
}
#endif
/*
* Copyright (C) the libgit2 contributors. All rights reserved.
*
* This file is part of libgit2, distributed under the GNU GPL v2 with
* a Linking Exception. For full terms see the included COPYING file.
*/
#ifndef INCLUDE_alloc_h__
#define INCLUDE_alloc_h__
#include "git2/sys/alloc.h"
extern git_allocator git__allocator;
#define git__malloc(len) git__allocator.gmalloc(len, __FILE__, __LINE__)
#define git__calloc(nelem, elsize) git__allocator.gcalloc(nelem, elsize, __FILE__, __LINE__)
#define git__strdup(str) git__allocator.gstrdup(str, __FILE__, __LINE__)
#define git__strndup(str, n) git__allocator.gstrndup(str, n, __FILE__, __LINE__)
#define git__substrdup(str, n) git__allocator.gsubstrdup(str, n, __FILE__, __LINE__)
#define git__realloc(ptr, size) git__allocator.grealloc(ptr, size, __FILE__, __LINE__)
#define git__reallocarray(ptr, nelem, elsize) git__allocator.greallocarray(ptr, nelem, elsize, __FILE__, __LINE__)
#define git__mallocarray(nelem, elsize) git__allocator.gmallocarray(nelem, elsize, __FILE__, __LINE__)
#define git__free git__allocator.gfree
/**
* This function is being called by our global setup routines to
* initialize the standard allocator.
*/
int git_allocator_global_init(void);
/**
* Switch out libgit2's global memory allocator
*
* @param allocator The new allocator that should be used. All function pointers
* of it need to be set correctly.
* @return An error code or 0.
*/
int git_allocator_setup(git_allocator *allocator);
#endif
......@@ -7,6 +7,7 @@
#include "global.h"
#include "alloc.h"
#include "hash.h"
#include "sysdir.h"
#include "filter.h"
......@@ -60,7 +61,8 @@ static int init_common(void)
#endif
/* Initialize any other subsystems that have global state */
if ((ret = git_hash_global_init()) == 0 &&
if ((ret = git_allocator_global_init()) == 0 &&
(ret = git_hash_global_init()) == 0 &&
(ret = git_sysdir_global_init()) == 0 &&
(ret = git_filter_global_init()) == 0 &&
(ret = git_merge_driver_global_init()) == 0 &&
......
......@@ -16,6 +16,7 @@
#endif
#include <git2.h>
#include "alloc.h"
#include "sysdir.h"
#include "cache.h"
#include "global.h"
......@@ -260,6 +261,10 @@ int git_libgit2_opts(int key, ...)
git_odb__strict_hash_verification = (va_arg(ap, int) != 0);
break;
case GIT_OPT_SET_ALLOCATOR:
error = git_allocator_setup(va_arg(ap, git_allocator *));
break;
default:
giterr_set(GITERR_INVALID, "invalid option key");
error = -1;
......
/*
* Copyright (C) the libgit2 contributors. All rights reserved.
*
* This file is part of libgit2, distributed under the GNU GPL v2 with
* a Linking Exception. For full terms see the included COPYING file.
*/
#include "stdalloc.h"
static void *stdalloc__malloc(size_t len, const char *file, int line)
{
void *ptr = malloc(len);
GIT_UNUSED(file);
GIT_UNUSED(line);
if (!ptr) giterr_set_oom();
return ptr;
}
static void *stdalloc__calloc(size_t nelem, size_t elsize, const char *file, int line)
{
void *ptr = calloc(nelem, elsize);
GIT_UNUSED(file);
GIT_UNUSED(line);
if (!ptr) giterr_set_oom();
return ptr;
}
static char *stdalloc__strdup(const char *str, const char *file, int line)
{
char *ptr = strdup(str);
GIT_UNUSED(file);
GIT_UNUSED(line);
if (!ptr) giterr_set_oom();
return ptr;
}
static char *stdalloc__strndup(const char *str, size_t n, const char *file, int line)
{
size_t length = 0, alloclength;
char *ptr;
length = p_strnlen(str, n);
if (GIT_ADD_SIZET_OVERFLOW(&alloclength, length, 1) ||
!(ptr = stdalloc__malloc(alloclength, file, line)))
return NULL;
if (length)
memcpy(ptr, str, length);
ptr[length] = '\0';
return ptr;
}
static char *stdalloc__substrdup(const char *start, size_t n, const char *file, int line)
{
char *ptr;
size_t alloclen;
if (GIT_ADD_SIZET_OVERFLOW(&alloclen, n, 1) ||
!(ptr = stdalloc__malloc(alloclen, file, line)))
return NULL;
memcpy(ptr, start, n);
ptr[n] = '\0';
return ptr;
}
static void *stdalloc__realloc(void *ptr, size_t size, const char *file, int line)
{
void *new_ptr = realloc(ptr, size);
GIT_UNUSED(file);
GIT_UNUSED(line);
if (!new_ptr) giterr_set_oom();
return new_ptr;
}
static void *stdalloc__reallocarray(void *ptr, size_t nelem, size_t elsize, const char *file, int line)
{
size_t newsize;
GIT_UNUSED(file);
GIT_UNUSED(line);
return GIT_MULTIPLY_SIZET_OVERFLOW(&newsize, nelem, elsize) ?
NULL : realloc(ptr, newsize);
}
static void *stdalloc__mallocarray(size_t nelem, size_t elsize, const char *file, int line)
{
return stdalloc__reallocarray(NULL, nelem, elsize, file, line);
}
static void stdalloc__free(void *ptr)
{
free(ptr);
}
int git_stdalloc_init_allocator(git_allocator *allocator)
{
allocator->gmalloc = stdalloc__malloc;
allocator->gcalloc = stdalloc__calloc;
allocator->gstrdup = stdalloc__strdup;
allocator->gstrndup = stdalloc__strndup;
allocator->gsubstrdup = stdalloc__substrdup;
allocator->grealloc = stdalloc__realloc;
allocator->greallocarray = stdalloc__reallocarray;
allocator->gmallocarray = stdalloc__mallocarray;
allocator->gfree = stdalloc__free;
return 0;
}
/*
* Copyright (C) the libgit2 contributors. All rights reserved.
*
* This file is part of libgit2, distributed under the GNU GPL v2 with
* a Linking Exception. For full terms see the included COPYING file.
*/
#ifndef INCLUDE_stdalloc_h__
#define INCLUDE_stdalloc_h__
#include "alloc.h"
#include "common.h"
int git_stdalloc_init_allocator(git_allocator *allocator);
#endif
......@@ -41,141 +41,6 @@
*/
#define CONST_STRLEN(x) ((sizeof(x)/sizeof(x[0])) - 1)
#if defined(GIT_MSVC_CRTDBG)
/* Enable MSVC CRTDBG memory leak reporting.
*
* We DO NOT use the "_CRTDBG_MAP_ALLOC" macro described in the MSVC
* documentation because all allocs/frees in libgit2 already go through
* the "git__" routines defined in this file. Simply using the normal
* reporting mechanism causes all leaks to be attributed to a routine
* here in util.h (ie, the actual call to calloc()) rather than the
* caller of git__calloc().
*
* Therefore, we declare a set of "git__crtdbg__" routines to replace
* the corresponding "git__" routines and re-define the "git__" symbols
* as macros. This allows us to get and report the file:line info of
* the real caller.
*
* We DO NOT replace the "git__free" routine because it needs to remain
* a function pointer because it is used as a function argument when
* setting up various structure "destructors".
*
* We also DO NOT use the "_CRTDBG_MAP_ALLOC" macro because it causes
* "free" to be remapped to "_free_dbg" and this causes problems for
* structures which define a field named "free".
*
* Finally, CRTDBG must be explicitly enabled and configured at program
* startup. See tests/main.c for an example.
*/
#include "win32/w32_crtdbg_stacktrace.h"
#define git__malloc(len) git__crtdbg__malloc(len, __FILE__, __LINE__)
#define git__calloc(nelem, elsize) git__crtdbg__calloc(nelem, elsize, __FILE__, __LINE__)
#define git__strdup(str) git__crtdbg__strdup(str, __FILE__, __LINE__)
#define git__strndup(str, n) git__crtdbg__strndup(str, n, __FILE__, __LINE__)
#define git__substrdup(str, n) git__crtdbg__substrdup(str, n, __FILE__, __LINE__)
#define git__realloc(ptr, size) git__crtdbg__realloc(ptr, size, __FILE__, __LINE__)
#define git__reallocarray(ptr, nelem, elsize) git__crtdbg__reallocarray(ptr, nelem, elsize, __FILE__, __LINE__)
#define git__mallocarray(nelem, elsize) git__crtdbg__mallocarray(nelem, elsize, __FILE__, __LINE__)
#else
/*
* Custom memory allocation wrappers
* that set error code and error message
* on allocation failure
*/
GIT_INLINE(void *) git__malloc(size_t len)
{
void *ptr = malloc(len);
if (!ptr) giterr_set_oom();
return ptr;
}
GIT_INLINE(void *) git__calloc(size_t nelem, size_t elsize)
{
void *ptr = calloc(nelem, elsize);
if (!ptr) giterr_set_oom();
return ptr;
}
GIT_INLINE(char *) git__strdup(const char *str)
{
char *ptr = strdup(str);
if (!ptr) giterr_set_oom();
return ptr;
}
GIT_INLINE(char *) git__strndup(const char *str, size_t n)
{
size_t length = 0, alloclength;
char *ptr;
length = p_strnlen(str, n);
if (GIT_ADD_SIZET_OVERFLOW(&alloclength, length, 1) ||
!(ptr = git__malloc(alloclength)))
return NULL;
if (length)
memcpy(ptr, str, length);
ptr[length] = '\0';
return ptr;
}
/* NOTE: This doesn't do null or '\0' checking. Watch those boundaries! */
GIT_INLINE(char *) git__substrdup(const char *start, size_t n)
{
char *ptr;
size_t alloclen;
if (GIT_ADD_SIZET_OVERFLOW(&alloclen, n, 1) ||
!(ptr = git__malloc(alloclen)))
return NULL;
memcpy(ptr, start, n);
ptr[n] = '\0';
return ptr;
}
GIT_INLINE(void *) git__realloc(void *ptr, size_t size)
{
void *new_ptr = realloc(ptr, size);
if (!new_ptr) giterr_set_oom();
return new_ptr;
}
/**
* Similar to `git__realloc`, except that it is suitable for reallocing an
* array to a new number of elements of `nelem`, each of size `elsize`.
* The total size calculation is checked for overflow.
*/
GIT_INLINE(void *) git__reallocarray(void *ptr, size_t nelem, size_t elsize)
{
size_t newsize;
return GIT_MULTIPLY_SIZET_OVERFLOW(&newsize, nelem, elsize) ?
NULL : realloc(ptr, newsize);
}
/**
* Similar to `git__calloc`, except that it does not zero memory.
*/
GIT_INLINE(void *) git__mallocarray(size_t nelem, size_t elsize)
{
return git__reallocarray(NULL, nelem, elsize);
}
#endif /* !MSVC_CTRDBG */
GIT_INLINE(void) git__free(void *ptr)
{
free(ptr);
}
#define STRCMP_CASESELECT(IGNORE_CASE, STR1, STR2) \
((IGNORE_CASE) ? strcasecmp((STR1), (STR2)) : strcmp((STR1), (STR2)))
......@@ -553,4 +418,6 @@ GIT_INLINE(double) git__timer(void)
extern int git__getenv(git_buf *out, const char *name);
#include "alloc.h"
#endif
......@@ -71,6 +71,99 @@ static bool g_limit_reached = false; /* had allocs after we filled row table */
static unsigned int g_checkpoint_id = 0; /* to better label leak checkpoints */
static bool g_transient_leaks_since_mark = false; /* payload for hook */
static void *crtdbg__malloc(size_t len, const char *file, int line)
{
void *ptr = _malloc_dbg(len, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
if (!ptr) giterr_set_oom();
return ptr;
}
static void *crtdbg__calloc(size_t nelem, size_t elsize, const char *file, int line)
{
void *ptr = _calloc_dbg(nelem, elsize, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
if (!ptr) giterr_set_oom();
return ptr;
}
static char *crtdbg__strdup(const char *str, const char *file, int line)
{
char *ptr = _strdup_dbg(str, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
if (!ptr) giterr_set_oom();
return ptr;
}
static char *crtdbg__strndup(const char *str, size_t n, const char *file, int line)
{
size_t length = 0, alloclength;
char *ptr;
length = p_strnlen(str, n);
if (GIT_ADD_SIZET_OVERFLOW(&alloclength, length, 1) ||
!(ptr = crtdbg__malloc(alloclength, file, line)))
return NULL;
if (length)
memcpy(ptr, str, length);
ptr[length] = '\0';
return ptr;
}
static char *crtdbg__substrdup(const char *start, size_t n, const char *file, int line)
{
char *ptr;
size_t alloclen;
if (GIT_ADD_SIZET_OVERFLOW(&alloclen, n, 1) ||
!(ptr = crtdbg__malloc(alloclen, file, line)))
return NULL;
memcpy(ptr, start, n);
ptr[n] = '\0';
return ptr;
}
static void *crtdbg__realloc(void *ptr, size_t size, const char *file, int line)
{
void *new_ptr = _realloc_dbg(ptr, size, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
if (!new_ptr) giterr_set_oom();
return new_ptr;
}
static void *crtdbg__reallocarray(void *ptr, size_t nelem, size_t elsize, const char *file, int line)
{
size_t newsize;
return GIT_MULTIPLY_SIZET_OVERFLOW(&newsize, nelem, elsize) ?
NULL : _realloc_dbg(ptr, newsize, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
}
static void *crtdbg__mallocarray(size_t nelem, size_t elsize, const char *file, int line)
{
return crtdbg__reallocarray(NULL, nelem, elsize, file, line);
}
static void crtdbg__free(void *ptr)
{
free(ptr);
}
int git_win32_crtdbg_init_allocator(git_allocator *allocator)
{
allocator->gmalloc = crtdbg__malloc;
allocator->gcalloc = crtdbg__calloc;
allocator->gstrdup = crtdbg__strdup;
allocator->gstrndup = crtdbg__strndup;
allocator->gsubstrdup = crtdbg__substrdup;
allocator->grealloc = crtdbg__realloc;
allocator->greallocarray = crtdbg__reallocarray;
allocator->gmallocarray = crtdbg__mallocarray;
allocator->gfree = crtdbg__free;
return 0;
}
/**
* Compare function for bsearch on g_cs_index table.
*/
......@@ -341,4 +434,5 @@ const char *git_win32__crtdbg_stacktrace(int skip, const char *file)
return result;
}
#endif
......@@ -17,6 +17,34 @@
#include "git2/errors.h"
#include "strnlen.h"
/* MSVC CRTDBG memory leak reporting.
*
* We DO NOT use the "_CRTDBG_MAP_ALLOC" macro described in the MSVC
* documentation because all allocs/frees in libgit2 already go through
* the "git__" routines defined in this file. Simply using the normal
* reporting mechanism causes all leaks to be attributed to a routine
* here in util.h (ie, the actual call to calloc()) rather than the
* caller of git__calloc().
*
* Therefore, we declare a set of "git__crtdbg__" routines to replace
* the corresponding "git__" routines and re-define the "git__" symbols
* as macros. This allows us to get and report the file:line info of
* the real caller.
*
* We DO NOT replace the "git__free" routine because it needs to remain
* a function pointer because it is used as a function argument when
* setting up various structure "destructors".
*
* We also DO NOT use the "_CRTDBG_MAP_ALLOC" macro because it causes
* "free" to be remapped to "_free_dbg" and this causes problems for
* structures which define a field named "free".
*
* Finally, CRTDBG must be explicitly enabled and configured at program
* startup. See tests/main.c for an example.
*/
int git_win32_crtdbg_init_allocator(git_allocator *allocator);
/**
* Initialize our memory leak tracking and de-dup data structures.
* This should ONLY be called by git_libgit2_init().
......@@ -97,80 +125,5 @@ GIT_EXTERN(int) git_win32__crtdbg_stacktrace__dump(
*/
const char *git_win32__crtdbg_stacktrace(int skip, const char *file);
GIT_INLINE(void *) git__crtdbg__malloc(size_t len, const char *file, int line)
{
void *ptr = _malloc_dbg(len, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
if (!ptr) giterr_set_oom();
return ptr;
}
GIT_INLINE(void *) git__crtdbg__calloc(size_t nelem, size_t elsize, const char *file, int line)
{
void *ptr = _calloc_dbg(nelem, elsize, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
if (!ptr) giterr_set_oom();
return ptr;
}
GIT_INLINE(char *) git__crtdbg__strdup(const char *str, const char *file, int line)
{
char *ptr = _strdup_dbg(str, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
if (!ptr) giterr_set_oom();
return ptr;
}
GIT_INLINE(char *) git__crtdbg__strndup(const char *str, size_t n, const char *file, int line)
{
size_t length = 0, alloclength;
char *ptr;
length = p_strnlen(str, n);
if (GIT_ADD_SIZET_OVERFLOW(&alloclength, length, 1) ||
!(ptr = git__crtdbg__malloc(alloclength, file, line)))
return NULL;
if (length)
memcpy(ptr, str, length);
ptr[length] = '\0';
return ptr;
}
GIT_INLINE(char *) git__crtdbg__substrdup(const char *start, size_t n, const char *file, int line)
{
char *ptr;
size_t alloclen;
if (GIT_ADD_SIZET_OVERFLOW(&alloclen, n, 1) ||
!(ptr = git__crtdbg__malloc(alloclen, file, line)))
return NULL;
memcpy(ptr, start, n);
ptr[n] = '\0';
return ptr;
}
GIT_INLINE(void *) git__crtdbg__realloc(void *ptr, size_t size, const char *file, int line)
{
void *new_ptr = _realloc_dbg(ptr, size, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
if (!new_ptr) giterr_set_oom();
return new_ptr;
}
GIT_INLINE(void *) git__crtdbg__reallocarray(void *ptr, size_t nelem, size_t elsize, const char *file, int line)
{
size_t newsize;
return GIT_MULTIPLY_SIZET_OVERFLOW(&newsize, nelem, elsize) ?
NULL : _realloc_dbg(ptr, newsize, _NORMAL_BLOCK, git_win32__crtdbg_stacktrace(1,file), line);
}
GIT_INLINE(void *) git__crtdbg__mallocarray(size_t nelem, size_t elsize, const char *file, int line)
{
return git__crtdbg__reallocarray(NULL, nelem, elsize, file, line);
}
#endif
#endif
#include "clar_libgit2.h"
#include "win32/w32_stack.h"
#include "win32/w32_crtdbg_stacktrace.h"
#if defined(GIT_MSVC_CRTDBG)
static void a(void)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment