Merge branch 'fix/w8-authroot' into fix/w8-integration
This commit is contained in:
+7
-27
@@ -30,8 +30,6 @@
|
|||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <openssl/x509.h>
|
#include <openssl/x509.h>
|
||||||
|
|
||||||
static char* authorized_root;
|
|
||||||
static int authorized_root_fd = -1;
|
|
||||||
static bool allow_delete;
|
static bool allow_delete;
|
||||||
static bool trust_sender;
|
static bool trust_sender;
|
||||||
static bool allow_unauthenticated;
|
static bool allow_unauthenticated;
|
||||||
@@ -201,13 +199,10 @@ static bool tls_client_identity_allowed(SSL* ssl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void release_authorization(void) {
|
static void release_authorization(void) {
|
||||||
file_set_authorized_root(-1, NULL);
|
int root_fd = utils_get_authorized_root_fd();
|
||||||
utils_set_authorized_root_fd(-1);
|
utils_set_authorized_root(-1, NULL);
|
||||||
if (authorized_root_fd >= 0)
|
if (root_fd >= 0)
|
||||||
close(authorized_root_fd);
|
close(root_fd);
|
||||||
authorized_root_fd = -1;
|
|
||||||
free(authorized_root);
|
|
||||||
authorized_root = NULL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool path_is_within(const char* root, const char* path) {
|
static bool path_is_within(const char* root, const char* path) {
|
||||||
@@ -233,13 +228,11 @@ static bool ensure_receive_root(const Config* config) {
|
|||||||
static bool configure_authorization(const char* root) {
|
static bool configure_authorization(const char* root) {
|
||||||
char resolved[PATH_MAX];
|
char resolved[PATH_MAX];
|
||||||
if (!root) {
|
if (!root) {
|
||||||
file_set_authorized_root(-1, NULL);
|
|
||||||
utils_set_authorized_root(-1, NULL);
|
utils_set_authorized_root(-1, NULL);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int root_fd = open(root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
int root_fd = open(root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||||
if (root_fd < 0) {
|
if (root_fd < 0) {
|
||||||
file_set_authorized_root(-1, NULL);
|
|
||||||
utils_set_authorized_root(-1, NULL);
|
utils_set_authorized_root(-1, NULL);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -248,26 +241,12 @@ static bool configure_authorization(const char* root) {
|
|||||||
if (fd_path_length < 0 || (size_t)fd_path_length >= sizeof(fd_path) ||
|
if (fd_path_length < 0 || (size_t)fd_path_length >= sizeof(fd_path) ||
|
||||||
!realpath(fd_path, resolved)) {
|
!realpath(fd_path, resolved)) {
|
||||||
close(root_fd);
|
close(root_fd);
|
||||||
file_set_authorized_root(-1, NULL);
|
|
||||||
utils_set_authorized_root(-1, NULL);
|
utils_set_authorized_root(-1, NULL);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
authorized_root = str_dup(resolved);
|
if (!utils_set_authorized_root(root_fd, resolved)) {
|
||||||
if (!authorized_root) {
|
/* The setter already cleared the fd/path state on allocation failure. */
|
||||||
close(root_fd);
|
close(root_fd);
|
||||||
file_set_authorized_root(-1, NULL);
|
|
||||||
utils_set_authorized_root(-1, NULL);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
authorized_root_fd = root_fd;
|
|
||||||
if (!file_set_authorized_root(authorized_root_fd, authorized_root) ||
|
|
||||||
!utils_set_authorized_root(authorized_root_fd, authorized_root)) {
|
|
||||||
file_set_authorized_root(-1, NULL);
|
|
||||||
utils_set_authorized_root(-1, NULL);
|
|
||||||
close(authorized_root_fd);
|
|
||||||
authorized_root_fd = -1;
|
|
||||||
free(authorized_root);
|
|
||||||
authorized_root = NULL;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -713,6 +692,7 @@ void handler(int file_descriptor) {
|
|||||||
* in effect. A client's --timeout tightens only that client's own protocol
|
* in effect. A client's --timeout tightens only that client's own protocol
|
||||||
* I/O and the server's socket read/write timeout is the transport default. */
|
* I/O and the server's socket read/write timeout is the transport default. */
|
||||||
protocol_session_set_io_timeout(&session, config->timeout);
|
protocol_session_set_io_timeout(&session, config->timeout);
|
||||||
|
const char* authorized_root = utils_get_authorized_root_path();
|
||||||
if (!authorized_root) {
|
if (!authorized_root) {
|
||||||
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
||||||
goto done;
|
goto done;
|
||||||
|
|||||||
+17
-29
@@ -284,23 +284,6 @@ size_t file_content_to_buffer(File* file) {
|
|||||||
|
|
||||||
/* ---- Secure filesystem primitives ---- */
|
/* ---- Secure filesystem primitives ---- */
|
||||||
|
|
||||||
static int authorized_root_fd = -1;
|
|
||||||
static char* authorized_root_path;
|
|
||||||
|
|
||||||
bool file_set_authorized_root(int fd, const char* canonical_path) {
|
|
||||||
char* path_copy = canonical_path ? str_dup(canonical_path) : NULL;
|
|
||||||
if (canonical_path && !path_copy) {
|
|
||||||
authorized_root_fd = -1;
|
|
||||||
free(authorized_root_path);
|
|
||||||
authorized_root_path = NULL;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
authorized_root_fd = fd;
|
|
||||||
free(authorized_root_path);
|
|
||||||
authorized_root_path = path_copy;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool file_path_exists_secure(const char* path) {
|
bool file_path_exists_secure(const char* path) {
|
||||||
if (!path)
|
if (!path)
|
||||||
return false;
|
return false;
|
||||||
@@ -483,7 +466,10 @@ static int open_dir_beneath_root(const char* resolved, const char* root) {
|
|||||||
rel++;
|
rel++;
|
||||||
if (*rel == '\0')
|
if (*rel == '\0')
|
||||||
return -1;
|
return -1;
|
||||||
int fd = dup(authorized_root_fd);
|
int root_fd = utils_get_authorized_root_fd();
|
||||||
|
if (root_fd < 0)
|
||||||
|
return -1;
|
||||||
|
int fd = dup(root_fd);
|
||||||
if (fd < 0)
|
if (fd < 0)
|
||||||
return -1;
|
return -1;
|
||||||
char* copy = str_dup(rel);
|
char* copy = str_dup(rel);
|
||||||
@@ -525,20 +511,21 @@ int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
int fd;
|
int fd;
|
||||||
if (authorized_root_fd >= 0) {
|
int root_fd = utils_get_authorized_root_fd();
|
||||||
if (!authorized_root_path || path[0] != '/' ||
|
const char* root_path = utils_get_authorized_root_path();
|
||||||
!path_is_within_root(authorized_root_path, path)) {
|
if (root_fd >= 0) {
|
||||||
|
if (!root_path || path[0] != '/' || !path_is_within_root(root_path, path)) {
|
||||||
free(copy);
|
free(copy);
|
||||||
free(leaf);
|
free(leaf);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
fd = dup(authorized_root_fd);
|
fd = dup(root_fd);
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
free(copy);
|
free(copy);
|
||||||
free(leaf);
|
free(leaf);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
size_t root_len = strlen(authorized_root_path);
|
size_t root_len = strlen(root_path);
|
||||||
char* relative = str_dup(path + root_len);
|
char* relative = str_dup(path + root_len);
|
||||||
if (!relative) {
|
if (!relative) {
|
||||||
free(copy);
|
free(copy);
|
||||||
@@ -602,15 +589,14 @@ int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs)
|
|||||||
O_NOFOLLOW walk. Only honoured when the symlink resolves to a
|
O_NOFOLLOW walk. Only honoured when the symlink resolves to a
|
||||||
directory that stays beneath the authorized root, so a malicious link
|
directory that stays beneath the authorized root, so a malicious link
|
||||||
can never redirect the write outside it. */
|
can never redirect the write outside it. */
|
||||||
if (next < 0 && file_keep_dirlinks && authorized_root_path != NULL &&
|
if (next < 0 && file_keep_dirlinks && root_path != NULL &&
|
||||||
(errno == ELOOP || errno == ENOTDIR || errno == EACCES)) {
|
(errno == ELOOP || errno == ENOTDIR || errno == EACCES)) {
|
||||||
struct stat lst;
|
struct stat lst;
|
||||||
if (fstatat(fd, component, &lst, AT_SYMLINK_NOFOLLOW) == 0 && S_ISLNK(lst.st_mode)) {
|
if (fstatat(fd, component, &lst, AT_SYMLINK_NOFOLLOW) == 0 && S_ISLNK(lst.st_mode)) {
|
||||||
char candidate[PATH_MAX];
|
char candidate[PATH_MAX];
|
||||||
char root[PATH_MAX];
|
char root[PATH_MAX];
|
||||||
if (realpath(authorized_root_path, root) &&
|
if (realpath(root_path, root) && snprintf(candidate, sizeof(candidate), "%s%s/%s", root,
|
||||||
snprintf(candidate, sizeof(candidate), "%s%s/%s", root, rel_buf, component) <
|
rel_buf, component) < (int)sizeof(candidate)) {
|
||||||
(int)sizeof(candidate)) {
|
|
||||||
char resolved[PATH_MAX];
|
char resolved[PATH_MAX];
|
||||||
if (realpath(candidate, resolved) && strcmp(resolved, root) != 0 &&
|
if (realpath(candidate, resolved) && strcmp(resolved, root) != 0 &&
|
||||||
strncmp(root, resolved, strlen(root)) == 0 &&
|
strncmp(root, resolved, strlen(root)) == 0 &&
|
||||||
@@ -685,8 +671,9 @@ bool file_ensure_directory_secure(const char* path) {
|
|||||||
return false;
|
return false;
|
||||||
/* The authorized root is already an open directory, and the filesystem root
|
/* The authorized root is already an open directory, and the filesystem root
|
||||||
is always present: there is no final component left to create for them. */
|
is always present: there is no final component left to create for them. */
|
||||||
|
const char* root_path = utils_get_authorized_root_path();
|
||||||
bool root_is_open =
|
bool root_is_open =
|
||||||
authorized_root_fd >= 0 && authorized_root_path && strcmp(norm, authorized_root_path) == 0;
|
utils_get_authorized_root_fd() >= 0 && root_path && strcmp(norm, root_path) == 0;
|
||||||
if (root_is_open || strcmp(norm, "/") == 0) {
|
if (root_is_open || strcmp(norm, "/") == 0) {
|
||||||
free(norm);
|
free(norm);
|
||||||
return true;
|
return true;
|
||||||
@@ -733,8 +720,9 @@ bool file_directory_exists_secure(const char* path) {
|
|||||||
char* norm = normalize_directory_path(path);
|
char* norm = normalize_directory_path(path);
|
||||||
if (!norm)
|
if (!norm)
|
||||||
return false;
|
return false;
|
||||||
|
const char* root_path = utils_get_authorized_root_path();
|
||||||
bool root_is_open =
|
bool root_is_open =
|
||||||
authorized_root_fd >= 0 && authorized_root_path && strcmp(norm, authorized_root_path) == 0;
|
utils_get_authorized_root_fd() >= 0 && root_path && strcmp(norm, root_path) == 0;
|
||||||
if (root_is_open || strcmp(norm, "/") == 0) {
|
if (root_is_open || strcmp(norm, "/") == 0) {
|
||||||
free(norm);
|
free(norm);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -62,9 +62,6 @@ void file_set_keep_dirlinks(bool enable);
|
|||||||
void file_set_trust_sender(bool enable);
|
void file_set_trust_sender(bool enable);
|
||||||
bool file_get_trust_sender(void);
|
bool file_get_trust_sender(void);
|
||||||
|
|
||||||
/* A configured fd without a canonical identity deliberately rejects paths. */
|
|
||||||
bool file_set_authorized_root(int fd, const char* canonical_path);
|
|
||||||
|
|
||||||
/* Secure path/filesystem primitives (symlink-safe, O_NOFOLLOW, root-confined). */
|
/* Secure path/filesystem primitives (symlink-safe, O_NOFOLLOW, root-confined). */
|
||||||
bool file_path_exists_secure(const char* path);
|
bool file_path_exists_secure(const char* path);
|
||||||
bool file_stat_secure(const char* path, struct stat* st);
|
bool file_stat_secure(const char* path, struct stat* st);
|
||||||
|
|||||||
+20
-7
@@ -36,6 +36,17 @@ void utils_set_authorized_root_fd(int fd) {
|
|||||||
(void)utils_set_authorized_root(fd, NULL);
|
(void)utils_set_authorized_root(fd, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Accessors for the process-global authorized root. The path pointer is
|
||||||
|
* borrowed and valid until the next setter call; the root is a single-threaded,
|
||||||
|
* set-before-worker-threads value (see server.c), so these carry no locking. */
|
||||||
|
int utils_get_authorized_root_fd(void) {
|
||||||
|
return authorized_root_fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* utils_get_authorized_root_path(void) {
|
||||||
|
return authorized_root_path;
|
||||||
|
}
|
||||||
|
|
||||||
bool path_is_within_root(const char* root, const char* path) {
|
bool path_is_within_root(const char* root, const char* path) {
|
||||||
size_t root_len = strlen(root);
|
size_t root_len = strlen(root);
|
||||||
return strncmp(root, path, root_len) == 0 && (path[root_len] == '\0' || path[root_len] == '/');
|
return strncmp(root, path, root_len) == 0 && (path[root_len] == '\0' || path[root_len] == '/');
|
||||||
@@ -50,15 +61,16 @@ bool path_is_within_root(const char* root, const char* path) {
|
|||||||
* in the extra receiver policies they apply, so they are intentionally kept
|
* in the extra receiver policies they apply, so they are intentionally kept
|
||||||
* separate. Both rely on the shared lexical path_is_within_root check. */
|
* separate. Both rely on the shared lexical path_is_within_root check. */
|
||||||
static int open_authorized_destination(const char* dest_root) {
|
static int open_authorized_destination(const char* dest_root) {
|
||||||
if (authorized_root_fd < 0 || !authorized_root_path || !dest_root ||
|
int root_fd = utils_get_authorized_root_fd();
|
||||||
!path_is_within_root(authorized_root_path, dest_root))
|
const char* root_path = utils_get_authorized_root_path();
|
||||||
|
if (root_fd < 0 || !root_path || !dest_root || !path_is_within_root(root_path, dest_root))
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
int dirfd = dup(authorized_root_fd);
|
int dirfd = dup(root_fd);
|
||||||
if (dirfd < 0)
|
if (dirfd < 0)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
const char* relative_path = dest_root + strlen(authorized_root_path);
|
const char* relative_path = dest_root + strlen(root_path);
|
||||||
while (*relative_path == '/')
|
while (*relative_path == '/')
|
||||||
relative_path++;
|
relative_path++;
|
||||||
char* relative = str_dup(*relative_path ? relative_path : ".");
|
char* relative = str_dup(*relative_path ? relative_path : ".");
|
||||||
@@ -689,11 +701,12 @@ DeleteWalkResult delete_extras_limited(const char* dest_root, const ArrayList* m
|
|||||||
if (!build_keep_index(manifest, &keep))
|
if (!build_keep_index(manifest, &keep))
|
||||||
return DELETE_WALK_ERROR;
|
return DELETE_WALK_ERROR;
|
||||||
int rootfd;
|
int rootfd;
|
||||||
if (authorized_root_fd >= 0) {
|
int root_fd = utils_get_authorized_root_fd();
|
||||||
if (authorized_root_path)
|
if (root_fd >= 0) {
|
||||||
|
if (utils_get_authorized_root_path())
|
||||||
rootfd = open_authorized_destination(dest_root);
|
rootfd = open_authorized_destination(dest_root);
|
||||||
else if (dest_root == NULL)
|
else if (dest_root == NULL)
|
||||||
rootfd = dup(authorized_root_fd);
|
rootfd = dup(root_fd);
|
||||||
else
|
else
|
||||||
rootfd = -1;
|
rootfd = -1;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -127,6 +127,21 @@ bool utils_set_authorized_root(int fd, const char* canonical_path);
|
|||||||
/* The fd-only compatibility form is fail-closed for path-based operations;
|
/* The fd-only compatibility form is fail-closed for path-based operations;
|
||||||
* callers should use utils_set_authorized_root with the canonical identity. */
|
* callers should use utils_set_authorized_root with the canonical identity. */
|
||||||
void utils_set_authorized_root_fd(int fd);
|
void utils_set_authorized_root_fd(int fd);
|
||||||
|
/* Read accessors for the process-wide authorized root, so every secure-walk
|
||||||
|
* site consumes the single shared state instead of keeping its own copy. The
|
||||||
|
* fd is caller-owned (see the setters): it is returned verbatim, never dup'd,
|
||||||
|
* and the caller that opened it is responsible for closing it. With no root
|
||||||
|
* configured the fd accessor returns -1 and the path accessor returns NULL.
|
||||||
|
*
|
||||||
|
* The pointer returned by utils_get_authorized_root_path() is borrowed into
|
||||||
|
* process-global state and is invalidated by the next
|
||||||
|
* utils_set_authorized_root() / utils_set_authorized_root_fd() call. The fd
|
||||||
|
* and path are stored separately and read independently, so the pair is NOT
|
||||||
|
* observed atomically together; the accessors are non-reentrant and callers
|
||||||
|
* must serialize configuration (the server installs the root before any worker
|
||||||
|
* threads spawn; see utils.c). */
|
||||||
|
int utils_get_authorized_root_fd(void);
|
||||||
|
const char* utils_get_authorized_root_path(void);
|
||||||
/* True when `path` is `root` itself or lies directly beneath it: a lexical
|
/* True when `path` is `root` itself or lies directly beneath it: a lexical
|
||||||
* prefix test requiring the byte after `root` to be '\0' or '/'. Both `root`
|
* prefix test requiring the byte after `root` to be '\0' or '/'. Both `root`
|
||||||
* and `path` must be absolute canonical paths free of "."/".." components (the
|
* and `path` must be absolute canonical paths free of "."/".." components (the
|
||||||
|
|||||||
+8
-8
@@ -1180,7 +1180,7 @@ static void test_trust_sender_authorized_root_confinement() {
|
|||||||
rmdir(sibling);
|
rmdir(sibling);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
EXPECT_TRUE(file_set_authorized_root(root_fd, root_abs));
|
EXPECT_TRUE(utils_set_authorized_root(root_fd, root_abs));
|
||||||
|
|
||||||
file_set_trust_sender(true);
|
file_set_trust_sender(true);
|
||||||
struct stat st;
|
struct stat st;
|
||||||
@@ -1204,7 +1204,7 @@ static void test_trust_sender_authorized_root_confinement() {
|
|||||||
|
|
||||||
free(outside_link);
|
free(outside_link);
|
||||||
free(inside_link);
|
free(inside_link);
|
||||||
file_set_authorized_root(-1, NULL);
|
utils_set_authorized_root(-1, NULL);
|
||||||
close(root_fd);
|
close(root_fd);
|
||||||
unlink("test_trust_sender_outside_link");
|
unlink("test_trust_sender_outside_link");
|
||||||
rmdir(sibling);
|
rmdir(sibling);
|
||||||
@@ -1220,7 +1220,7 @@ void test_trust_sender() {
|
|||||||
test_trust_sender_confines_hostile_paths();
|
test_trust_sender_confines_hostile_paths();
|
||||||
test_trust_sender_authorized_root_confinement();
|
test_trust_sender_authorized_root_confinement();
|
||||||
file_set_trust_sender(false);
|
file_set_trust_sender(false);
|
||||||
file_set_authorized_root(-1, NULL);
|
utils_set_authorized_root(-1, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --sparse/-S hole preservation: a buffer with a long zero run written via
|
/* --sparse/-S hole preservation: a buffer with a long zero run written via
|
||||||
@@ -1341,7 +1341,7 @@ static void test_file_write_to_disk_partial_retention() {
|
|||||||
static void test_dir_time_list() {
|
static void test_dir_time_list() {
|
||||||
const char* root = "test_dir_time_root";
|
const char* root = "test_dir_time_root";
|
||||||
const char* sub = "test_dir_time_root/sub";
|
const char* sub = "test_dir_time_root/sub";
|
||||||
file_set_authorized_root(-1, NULL);
|
utils_set_authorized_root(-1, NULL);
|
||||||
rmdir(sub);
|
rmdir(sub);
|
||||||
rmdir(root);
|
rmdir(root);
|
||||||
EXPECT_EQ_INT(mkdir(root, 0755), 0);
|
EXPECT_EQ_INT(mkdir(root, 0755), 0);
|
||||||
@@ -1485,7 +1485,7 @@ static void test_keep_dirlinks_secure_open_impl() {
|
|||||||
rmdir(outside);
|
rmdir(outside);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
EXPECT_TRUE(file_set_authorized_root(root_fd, root_abs));
|
EXPECT_TRUE(utils_set_authorized_root(root_fd, root_abs));
|
||||||
file_set_keep_dirlinks(true);
|
file_set_keep_dirlinks(true);
|
||||||
|
|
||||||
struct stat real_st;
|
struct stat real_st;
|
||||||
@@ -1538,7 +1538,7 @@ static void test_keep_dirlinks_secure_open_impl() {
|
|||||||
free(leaf);
|
free(leaf);
|
||||||
|
|
||||||
file_set_keep_dirlinks(false);
|
file_set_keep_dirlinks(false);
|
||||||
file_set_authorized_root(-1, NULL);
|
utils_set_authorized_root(-1, NULL);
|
||||||
close(root_fd);
|
close(root_fd);
|
||||||
unlink(link);
|
unlink(link);
|
||||||
unlink(abslink);
|
unlink(abslink);
|
||||||
@@ -1552,10 +1552,10 @@ static void test_keep_dirlinks_secure_open_impl() {
|
|||||||
* cleared even when an EXPECT inside the body returns early (a failing EXPECT
|
* cleared even when an EXPECT inside the body returns early (a failing EXPECT
|
||||||
* returns from its own function, so the body's trailing resets may be skipped). */
|
* returns from its own function, so the body's trailing resets may be skipped). */
|
||||||
static void test_keep_dirlinks_secure_open() {
|
static void test_keep_dirlinks_secure_open() {
|
||||||
file_set_authorized_root(-1, NULL);
|
utils_set_authorized_root(-1, NULL);
|
||||||
file_set_keep_dirlinks(false);
|
file_set_keep_dirlinks(false);
|
||||||
test_keep_dirlinks_secure_open_impl();
|
test_keep_dirlinks_secure_open_impl();
|
||||||
file_set_authorized_root(-1, NULL);
|
utils_set_authorized_root(-1, NULL);
|
||||||
file_set_keep_dirlinks(false);
|
file_set_keep_dirlinks(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user