symlink-trust: -k/--copy-dirlinks, -K/--keep-dirlinks, --munge-links (+real -l/--links)
CI / lint (pull_request) Successful in 1m1s
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
CI / build-and-test (pull_request) Successful in 1m19s

Adds symlink-target transmission (File is_symlink+symlink_target, STATUS_SYMLINK
frame, chunk type 2), munge-links sender containment + receiver-side symmetric
target containment, keep-dirlinks confined dir-symlink following (O_NOFOLLOW
realpath-rechecked), and fixes -l to copy symlinks as symlinks. munge_links +
keep_dirlinks cross the wire; copy_dirlinks client-only. PROTOCOL_VERSION
2.12.0->2.13.0. Review fixes: receiver rejects absolute/.. targets, gated unmunge,
-K O_NOFOLLOW+re-fstat, keep_dirlinks set once at config-accept, rel_buf overflow
fails the walk.
This commit is contained in:
2026-09-08 22:27:53 +02:00
parent 0de859b302
commit 820188c2cc
22 changed files with 945 additions and 39 deletions
+63 -4
View File
@@ -74,7 +74,8 @@ static unsigned long long per_file_serialize_size(File* file, bool use_metadata)
if (metadata_size > ULLONG_MAX - size)
return 0;
size += metadata_size;
/* Entry type marker: 0 = regular file, 1 = explicit directory entry. */
/* Entry type marker: 0 = regular file, 1 = explicit directory entry,
2 = symlink entry (carries its target string). */
if (sizeof(int) > ULLONG_MAX - size)
return 0;
size += sizeof(int);
@@ -83,7 +84,18 @@ static unsigned long long per_file_serialize_size(File* file, bool use_metadata)
size += sizeof(size_t);
if ((unsigned long long)file->data->size > ULLONG_MAX - size)
return 0;
return size + file->data->size;
size += file->data->size;
/* Symlink entries append the target string (length-prefixed). */
if (file->is_symlink) {
size_t target_len = file->symlink_target ? strlen(file->symlink_target) : 0;
if (sizeof(size_t) > ULLONG_MAX - size)
return 0;
size += sizeof(size_t);
if ((unsigned long long)target_len > ULLONG_MAX - size)
return 0;
size += target_len;
}
return size;
}
Data* chunk_serialize(Chunk* chunk, bool use_metadata) {
@@ -116,7 +128,7 @@ Data* chunk_serialize(Chunk* chunk, bool use_metadata) {
memcpy(data_pointer, wire_path, path_len);
data_pointer += path_len;
int entry_type = file->is_dir ? 1 : 0;
int entry_type = file->is_symlink ? 2 : (file->is_dir ? 1 : 0);
memcpy(data_pointer, &entry_type, sizeof(int));
data_pointer += sizeof(int);
@@ -129,6 +141,15 @@ Data* chunk_serialize(Chunk* chunk, bool use_metadata) {
if (file_data_size > 0)
memcpy(data_pointer, file->data->data, file_data_size);
data_pointer += file_data_size;
if (file->is_symlink) {
size_t target_len = file->symlink_target ? strlen(file->symlink_target) : 0;
memcpy(data_pointer, &target_len, sizeof(size_t));
data_pointer += sizeof(size_t);
if (target_len > 0)
memcpy(data_pointer, file->symlink_target, target_len);
data_pointer += target_len;
}
}
return data;
}
@@ -206,13 +227,14 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
}
int entry_type;
memcpy(&entry_type, data_pointer, sizeof(int));
if (entry_type != 0 && entry_type != 1) {
if (entry_type != 0 && entry_type != 1 && entry_type != 2) {
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: bad entry type");
file_destroy(file);
array_list_delete(files);
return NULL;
}
file->is_dir = entry_type == 1;
file->is_symlink = entry_type == 2;
data_pointer += sizeof(int);
remaining_size -= sizeof(int);
@@ -293,6 +315,43 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
data_pointer += file_data_size;
remaining_size -= file_data_size;
if (file->is_symlink) {
if (remaining_size < sizeof(size_t)) {
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for symlink target");
file_destroy(file);
array_list_delete(files);
return NULL;
}
size_t target_len;
memcpy(&target_len, data_pointer, sizeof(size_t));
data_pointer += sizeof(size_t);
remaining_size -= sizeof(size_t);
if (target_len == 0 || remaining_size < target_len) {
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: bad symlink target");
file_destroy(file);
array_list_delete(files);
return NULL;
}
char* target = protocol_alloc(target_len + 1);
if (!target) {
log_perror("Could not allocate memory for symlink target");
file_destroy(file);
array_list_delete(files);
return NULL;
}
memcpy(target, data_pointer, target_len);
target[target_len] = '\0';
if (memchr(target, '\0', target_len) != NULL) {
free(target);
file_destroy(file);
array_list_delete(files);
return NULL;
}
file->symlink_target = target;
data_pointer += target_len;
remaining_size -= target_len;
}
if (!array_list_add(files, file)) {
file_destroy(file);
array_list_delete(files);
+20 -2
View File
@@ -71,6 +71,9 @@ static void config_set_defaults(Config* config) {
config->copy_links = false;
config->safe_links = false;
config->copy_unsafe_links = false;
config->copy_dirlinks = false;
config->munge_links = false;
config->keep_dirlinks = false;
config->preserve_hard_links = false;
config->preserve_acls = false;
config->preserve_xattrs = false;
@@ -204,6 +207,7 @@ static bool validate_received_config(const Config* config) {
!(config->preserve_hard_links && (config->append || config->append_verify)) &&
valid_wire_bool(config->preserve_atimes) && valid_wire_bool(config->preserve_crtimes) &&
valid_wire_bool(config->omit_dir_times) && valid_wire_bool(config->omit_link_times) &&
valid_wire_bool(config->munge_links) && valid_wire_bool(config->keep_dirlinks) &&
(!config->use_compression ||
(config->compression_level >= 1 && config->compression_level <= 22)) &&
config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE &&
@@ -771,6 +775,18 @@ static bool receive_metadata_times_options(int fd, Config* c) {
receive_wire_bool(fd, &c->omit_link_times);
}
/* Phase 4 symlink-trust: --munge-links and -K/--keep-dirlinks. Both CROSS the
* wire (the receiver unmunges symlink targets and, with -K, follows an in-root
* destination symlink-to-directory). -k/--copy-dirlinks is sender-only and is
* never serialized. Trailing fields; protocol 2.13.0. */
static bool send_symlink_trust_options(int fd, const Config* c) {
return send_int(fd, c->munge_links) && send_int(fd, c->keep_dirlinks);
}
static bool receive_symlink_trust_options(int fd, Config* c) {
return receive_wire_bool(fd, &c->munge_links) && receive_wire_bool(fd, &c->keep_dirlinks);
}
bool config_send(int file_descriptor, const Config* config) {
protocol_session_set_max_alloc(NULL, config->max_alloc);
if (!send_core_fields(file_descriptor, config) || !send_delta_fields(file_descriptor, config) ||
@@ -780,7 +796,8 @@ bool config_send(int file_descriptor, const Config* config) {
!send_basis_options(file_descriptor, config) || !send_fuzzy_option(file_descriptor, config) ||
!send_checksum_options(file_descriptor, config) ||
!send_identity_options(file_descriptor, config) ||
!send_metadata_times_options(file_descriptor, config))
!send_metadata_times_options(file_descriptor, config) ||
!send_symlink_trust_options(file_descriptor, config))
return false;
Status status;
if (!receive_status(file_descriptor, &status))
@@ -817,7 +834,8 @@ Config* config_receive(int file_descriptor) {
!receive_fuzzy_option(file_descriptor, config) ||
!receive_checksum_options(file_descriptor, config) ||
!receive_identity_options(file_descriptor, config) ||
!receive_metadata_times_options(file_descriptor, config))
!receive_metadata_times_options(file_descriptor, config) ||
!receive_symlink_trust_options(file_descriptor, config))
goto error;
if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 &&
strcmp(config->compress_choice, "none") != 0) {
+10 -1
View File
@@ -108,6 +108,15 @@ typedef struct Config {
bool copy_links;
bool safe_links;
bool copy_unsafe_links;
/* Phase 4 symlink-trust. -k/--copy-dirlinks and --munge-links are
* CLIENT/sender-side only (they decide how the SENDER scans and rewrites
* symlinks; the receiver never reads them), so they never cross the wire.
* -K/--keep-dirlinks is a RECEIVER-side policy (follow an in-root destination
* symlink-to-directory as a directory) and CROSSES the wire along with
* --munge-links (so the receiver knows to unmunge). */
bool copy_dirlinks; /* client-only, sender-side (-k) */
bool munge_links; /* crosses the wire */
bool keep_dirlinks; /* crosses the wire (-K) */
// Issue #121: Extended metadata preservation
bool preserve_hard_links;
@@ -312,7 +321,7 @@ typedef struct Config {
bool open_noatime;
} Config;
#define PROTOCOL_VERSION "2.12.0"
#define PROTOCOL_VERSION "2.13.0"
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
#define MAX_BASIS_DIRS 64
+157 -1
View File
@@ -114,6 +114,8 @@ File* file_create(const char* path) {
file->link_group = 0;
file->link_first = false;
file->hardlink_target = NULL;
file->is_symlink = false;
file->symlink_target = NULL;
return file;
}
@@ -133,6 +135,8 @@ void file_destroy(void* item) {
file->basis_link = NULL;
free(file->hardlink_target);
file->hardlink_target = NULL;
free(file->symlink_target);
file->symlink_target = NULL;
free(file);
}
@@ -336,6 +340,108 @@ bool file_destination_is_newer_secure(const char* path, const FileMetadata* meta
return file_stat_secure(path, &st) && stat_is_newer(&st, metadata);
}
/* --keep-dirlinks (-K) receiver process-wide policy: when set, a destination
* path component that is itself a symlink to an in-root directory is followed
* (used as that directory) instead of failing the O_NOFOLLOW walk. Only ever
* honoured when the resolved target is a directory that stays beneath the
* authorized root, so a malicious symlink can never redirect the write outside
* it. Client of record is the server's receiver. */
static bool file_keep_dirlinks = false;
void file_set_keep_dirlinks(bool enable) {
file_keep_dirlinks = enable;
}
bool file_get_keep_dirlinks(void) {
return file_keep_dirlinks;
}
/* True when `target` is a lexical symlink target that can never escape the
* receive root once created beneath it: relative (not absolute) and containing
* no ".." path component. Used by --munge-links' sender-side containment: an
* escaping target is never transmitted (the entry is skipped/contained). */
bool file_symlink_target_contained(const char* target) {
if (!target || target[0] == '\0' || target[0] == '/')
return false;
const char* p = target;
while (*p) {
const char* slash = strchr(p, '/');
size_t comp_len = slash ? (size_t)(slash - p) : strlen(p);
if (comp_len == 2 && p[0] == '.' && p[1] == '.')
return false;
if (!slash)
break;
p = slash + 1;
}
return true;
}
/* Remove a leading symlink munge marker (if present); returns true when the
* marker was stripped. `target` is a mutable NUL-terminated buffer. */
bool file_symlink_unmunge(char* target) {
if (!target)
return false;
static const char* const marker = SYMLINK_MUNGE_PREFIX;
size_t marker_len = strlen(marker);
if (strncmp(target, marker, marker_len) != 0)
return false;
size_t rest = strlen(target + marker_len) + 1;
memmove(target, target + marker_len, rest);
return true;
}
/* Owned copy of `target` prefixed with SYMLINK_MUNGE_PREFIX (the sender-side
* --munge-links rewriting). Returns NULL on allocation failure. */
char* file_symlink_munge(const char* target) {
if (!target)
return NULL;
static const char* const marker = SYMLINK_MUNGE_PREFIX;
size_t marker_len = strlen(marker);
size_t target_len = strlen(target);
char* out = malloc(marker_len + target_len + 1);
if (!out)
return NULL;
memcpy(out, marker, marker_len);
memcpy(out + marker_len, target, target_len + 1);
return out;
}
/* Create a symlink at `path` pointing to `target`, confined below the
* authorized root: the parent directory is opened with an O_NOFOLLOW fd walk
* and the link is created with symlinkat so neither the destination chain nor
* the target is ever followed. The final component is never dereferenced: an
* existing non-directory entry at `path` is unlinked by name before the link is
* placed; an existing directory there is left untouched (returns false, so a
* caller can treat it as a collision). As a receiver-side trust-boundary
* invariant, `target` must be file_symlink_target_contained() (relative and
* ".."-free): an absolute or escaping target is rejected outright (returns
* false) so a malicious sender can never materialize a symlink that points
* outside the receive root. */
bool file_symlink_at_secure(const char* path, const char* target) {
if (!path || !target || has_path_traversal(path) || !file_symlink_target_contained(target))
return false;
char* leaf = NULL;
int parent_fd = file_open_secure_parent(path, &leaf, true);
if (parent_fd < 0)
return false;
bool ok = false;
struct stat st;
bool exists = fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) == 0;
if (exists && S_ISDIR(st.st_mode)) {
/* A directory already at this path cannot be replaced atomically with a
symlink without --force semantics; leave it and report the collision. */
ok = false;
} else {
if (exists && unlinkat(parent_fd, leaf, 0) != 0 && errno != ENOENT)
goto out;
ok = symlinkat(target, parent_fd, leaf) == 0;
}
out:
close(parent_fd);
free(leaf);
return ok;
}
int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs) {
char* copy = str_dup(path);
if (!copy)
@@ -383,6 +489,7 @@ int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs)
}
char* save = NULL;
char* component = strtok_r(parent, "/", &save);
char rel_buf[PATH_MAX] = "";
while (component) {
if (strcmp(component, "..") == 0) {
close(fd);
@@ -392,10 +499,45 @@ int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs)
}
if (strcmp(component, ".") != 0) {
int next = openat(fd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (create_dirs && next < 0 && errno == ENOENT) {
if (next < 0 && create_dirs && errno == ENOENT) {
if (mkdirat(fd, component, 0755) == 0 || errno == EEXIST)
next = openat(fd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
}
/* --keep-dirlinks (-K): a path component that is an existing symlink to
an in-root directory is used as THAT directory rather than failing the
O_NOFOLLOW walk. Only honoured when the symlink resolves to a
directory that stays beneath the authorized root, so a malicious link
can never redirect the write outside it. */
if (next < 0 && file_keep_dirlinks && authorized_root_path != NULL &&
(errno == ELOOP || errno == ENOTDIR || errno == EACCES)) {
struct stat lst;
if (fstatat(fd, component, &lst, AT_SYMLINK_NOFOLLOW) == 0 && S_ISLNK(lst.st_mode)) {
char candidate[PATH_MAX];
char root[PATH_MAX];
if (realpath(authorized_root_path, root) &&
snprintf(candidate, sizeof(candidate), "%s%s/%s", root, rel_buf, component) <
(int)sizeof(candidate)) {
char resolved[PATH_MAX];
if (realpath(candidate, resolved) && strcmp(resolved, root) != 0 &&
strncmp(root, resolved, strlen(root)) == 0 &&
(resolved[strlen(root)] == '/' || resolved[strlen(root)] == '\0')) {
struct stat rst;
if (stat(resolved, &rst) == 0 && S_ISDIR(rst.st_mode)) {
/* Re-open the resolved directory WITHOUT following a symlink and
re-verify it is still a directory inode, so a symlink swapped
in between realpath() and open() (TOCTOU) cannot redirect this
fd outside the root. */
next = open(resolved, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
struct stat ofst;
if (next >= 0 && (fstat(next, &ofst) != 0 || !S_ISDIR(ofst.st_mode))) {
close(next);
next = -1;
}
}
}
}
}
}
if (next < 0) {
close(fd);
free(copy);
@@ -404,6 +546,20 @@ int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs)
}
close(fd);
fd = next;
/* Track the walked relative prefix so the -K candidate path can be
reconstructed. An overflow while building it means the whole path is
at the PATH_MAX edge, so fail hard rather than silently building a
wrong (truncated) candidate for a later -K follow. */
size_t need = strlen(rel_buf) + strlen(component) + 2;
if (need <= sizeof(rel_buf)) {
strcat(rel_buf, "/");
strcat(rel_buf, component);
} else if (file_keep_dirlinks) {
close(fd);
free(copy);
free(leaf);
return -1;
}
}
component = strtok_r(NULL, "/", &save);
}
+21
View File
@@ -33,6 +33,27 @@ int file_open_for_read(const char* path);
bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size,
bool inplace, bool sparse);
/* Symlink trust-boundary helpers (Phase 4, symlink wave). --munge-links
* sender-side marker: every transmitted symlink target is prefixed with this
* while the flag is on; the receiver strips it to restore the real target. */
#define SYMLINK_MUNGE_PREFIX "#SYMLINK/"
char* file_symlink_munge(const char* target);
/* True when a lexical target is relative and contains no ".." component, so it
* can never escape the receive root once created beneath it. */
bool file_symlink_target_contained(const char* target);
/* Strip a leading SYMLINK_MUNGE_PREFIX from `target` (mutable, in place);
* returns true when a marker was removed. */
bool file_symlink_unmunge(char* target);
/* Create a symlink at `path` -> `target`, confined below the authorized root
* (O_NOFOLLOW parent walk, symlinkat; the target is never followed). Returns
* false when a directory already occupies `path`. */
bool file_symlink_at_secure(const char* path, const char* target);
/* --keep-dirlinks (-K) receiver process-wide policy: allow an in-root existing
* symlink-to-directory to be followed as a directory. */
void file_set_keep_dirlinks(bool enable);
bool file_get_keep_dirlinks(void);
/* A configured fd without a canonical identity deliberately rejects paths. */
bool file_set_authorized_root(int fd, const char* canonical_path);
+96
View File
@@ -332,6 +332,49 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_ERROR;
}
/* Symlink entry. (The process-wide --keep-dirlinks policy is set once by the
connection handler from the negotiated config, before any receiver/writer
threads start, so it is stable throughout this walk.) */
if (file->is_symlink) {
if (!file->symlink_target || file->path[0] == '\0' || has_path_traversal(file->path)) {
log_message(LOG_LEVEL_ERROR, "Invalid symlink entry received");
return FILE_SAVE_ERROR;
}
char* link_path = path_cat(root_directory, file->path);
if (!link_path)
return FILE_SAVE_ERROR;
/* Restore the real target by stripping the sender's --munge-links marker.
Only unmunge when the policy was negotiated: a plain -l run must preserve
a source symlink whose target genuinely begins with the marker verbatim. */
char* target = str_dup(file->symlink_target);
bool ok = target != NULL;
if (ok && config && config->munge_links)
file_symlink_unmunge(target);
/* Receiver-side trust boundary (independent of the sender): a target that
could escape the receive root (absolute, or relative-with-"..") is never
materialized. It is contained (the entry is skipped) rather than failing
the whole transfer, so a hostile sender can inject a broken symlink but
can never redirect it outside the root. */
if (ok && !file_symlink_target_contained(target))
ok = false;
if (!ok) {
/* Skip the escaping/empty target (contained) rather than abort. */
free(target);
free(link_path);
return FILE_SAVE_SKIPPED;
}
char* parent = str_dup(link_path);
if (parent) {
file_ensure_directory_secure(dirname(parent));
free(parent);
}
ok = file_symlink_at_secure(link_path, target);
free(target);
free(link_path);
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_ERROR;
}
/* --hard-links/-H sibling: a later member of a link group arrives with no
payload and is installed as a hard link to (or, on link() failure, a
byte-identical copy of) the group's first member. Handled entirely here,
@@ -1868,6 +1911,59 @@ File* file_receive_hardlink(int file_descriptor) {
return file;
}
/* Receive a symlink entry (the leading STATUS_SYMLINK code has already been
consumed): the destination path and the (sender-munged, if --munge-links)
symlink target string, then metadata when negotiated. The created File is
routed through the regular store_file sink, which creates the link beneath
the receive root (unmungeing the target first). */
File* file_receive_symlink(int file_descriptor, const Config* config) {
char* path = receive_str(file_descriptor);
if (path == NULL)
return NULL;
if (path[0] == '\0' || has_path_traversal(path)) {
char* escaped_path = output_escape(path, log_get_8_bit_output());
log_message(LOG_LEVEL_ERROR, "Invalid received symlink path: %s",
escaped_path ? escaped_path : "<allocation failed>");
free(escaped_path);
free(path);
send_status(file_descriptor, STATUS_ERROR);
return NULL;
}
char* target = receive_str(file_descriptor);
if (!target) {
free(path);
return NULL;
}
if (target[0] == '\0') {
char* escaped = output_escape(target, log_get_8_bit_output());
log_message(LOG_LEVEL_ERROR, "Invalid received symlink target: %s",
escaped ? escaped : "<allocation failed>");
free(escaped);
free(target);
free(path);
send_status(file_descriptor, STATUS_ERROR);
return NULL;
}
File* file = file_create(path);
free(path);
if (!file) {
free(target);
return NULL;
}
if (config && config->use_metadata) {
int meta_ok = 1;
file->metadata = metadata_receive(file_descriptor, &meta_ok);
if (!meta_ok) {
file_destroy(file);
free(target);
return NULL;
}
}
file->is_symlink = true;
file->symlink_target = target;
return file;
}
/* Read a delete-manifest frame (the STATUS_MANIFEST leading code has already
been consumed): a keep-set entry count followed by that many
destination-relative paths, then a protected-prefix count followed by that
+1
View File
@@ -10,6 +10,7 @@
File* file_receive(const Config* config, int file_descriptor);
File* file_receive_directory(int file_descriptor);
File* file_receive_hardlink(int file_descriptor);
File* file_receive_symlink(int file_descriptor, const Config* config);
File* receive_incremental_check(int fd, const Config* config, bool* skipped);
/* A received delete-manifest frame: the keep-set (`keeps`, destination-relative
+7
View File
@@ -56,6 +56,13 @@ typedef struct {
int link_group;
bool link_first;
char* hardlink_target;
/* Symlink-type entry (-l/--links, or -k/--copy-dirlinks' keep-as-symlink
* branch). When true, `symlink_target` holds the (sender-munged, if
* --munge-links) target string that is carried on the wire; the receiver
* creates a symlink to (an unmunged) target instead of writing regular-file
* data. `data` is empty for a symlink entry. Sender + receiver state. */
bool is_symlink;
char* symlink_target;
} File;
/* The path that should be sent on the wire and used for the receiver-side
+7 -1
View File
@@ -90,7 +90,13 @@ enum NET_STATUS {
* first (data-carrying) member's destination-relative wire path; the receiver
* creates this entry as a hard link to the first member's installed file
* (falling back to a byte-identical copy if link() fails). Protocol 2.12.0. */
STATUS_HARDLINK
STATUS_HARDLINK,
/* A symlink-type entry (-l/--links, -k/--copy-dirlinks' keep-as-symlink
* branch). The sender transmits the destination path, the (sender-munged,
* if --munge-links) symlink target, and optional metadata; the receiver
* creates a symlink to the unmunged target beneath the receive root (see
* file_receive_symlink). Protocol 2.13.0. */
STATUS_SYMLINK
};
void io_set_fds(int read_fd, int write_fd);