/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- * * Copyright (C) 2011,2013 Colin Walters * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * * Author: Colin Walters */ #include "config.h" #include #include #include #include #include "otutil.h" #include "ostree-repo-file.h" #include "ostree-sepolicy-private.h" #include "ostree-core-private.h" #include "ostree-repo-private.h" #define WHITEOUT_PREFIX ".wh." /* Per-checkout call state/caching */ typedef struct { GString *selabel_path_buf; } CheckoutState; static void checkout_state_clear (CheckoutState *state) { if (state->selabel_path_buf) g_string_free (state->selabel_path_buf, TRUE); } G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(CheckoutState, checkout_state_clear) static gboolean checkout_object_for_uncompressed_cache (OstreeRepo *self, const char *loose_path, GFileInfo *src_info, GInputStream *content, GCancellable *cancellable, GError **error) { /* Don't make setuid files in uncompressed cache */ guint32 file_mode = g_file_info_get_attribute_uint32 (src_info, "unix::mode"); file_mode &= ~(S_ISUID|S_ISGID); g_auto(GLnxTmpfile) tmpf = { 0, }; if (!glnx_open_tmpfile_linkable_at (self->tmp_dir_fd, ".", O_WRONLY | O_CLOEXEC, &tmpf, error)) return FALSE; g_autoptr(GOutputStream) temp_out = g_unix_output_stream_new (tmpf.fd, FALSE); if (g_output_stream_splice (temp_out, content, 0, cancellable, error) < 0) return FALSE; if (!g_output_stream_flush (temp_out, cancellable, error)) return FALSE; if (!self->disable_fsync) { if (TEMP_FAILURE_RETRY (fsync (tmpf.fd)) < 0) return glnx_throw_errno (error); } if (!g_output_stream_close (temp_out, cancellable, error)) return FALSE; if (!glnx_fchmod (tmpf.fd, file_mode, error)) return FALSE; if (!_ostree_repo_ensure_loose_objdir_at (self->uncompressed_objects_dir_fd, loose_path, cancellable, error)) return FALSE; if (!glnx_link_tmpfile_at (&tmpf, GLNX_LINK_TMPFILE_NOREPLACE_IGNORE_EXIST, self->uncompressed_objects_dir_fd, loose_path, error)) return FALSE; return TRUE; } static gboolean fsync_is_enabled (OstreeRepo *self, OstreeRepoCheckoutAtOptions *options) { return options->enable_fsync; } static gboolean write_regular_file_content (OstreeRepo *self, OstreeRepoCheckoutAtOptions *options, int outfd, GFileInfo *file_info, GVariant *xattrs, GInputStream *input, GCancellable *cancellable, GError **error) { const OstreeRepoCheckoutMode mode = options->mode; g_autoptr(GOutputStream) outstream = NULL; if (G_IS_FILE_DESCRIPTOR_BASED (input)) { int infd = g_file_descriptor_based_get_fd ((GFileDescriptorBased*) input); guint64 len = g_file_info_get_size (file_info); if (glnx_regfile_copy_bytes (infd, outfd, (off_t)len) < 0) return glnx_throw_errno_prefix (error, "regfile copy"); } else { outstream = g_unix_output_stream_new (outfd, FALSE); if (g_output_stream_splice (outstream, input, 0, cancellable, error) < 0) return FALSE; if (!g_output_stream_flush (outstream, cancellable, error)) return FALSE; } if (mode != OSTREE_REPO_CHECKOUT_MODE_USER) { if (TEMP_FAILURE_RETRY (fchown (outfd, g_file_info_get_attribute_uint32 (file_info, "unix::uid"), g_file_info_get_attribute_uint32 (file_info, "unix::gid"))) < 0) return glnx_throw_errno_prefix (error, "fchown"); if (xattrs) { if (!glnx_fd_set_all_xattrs (outfd, xattrs, cancellable, error)) return FALSE; } } guint32 file_mode = g_file_info_get_attribute_uint32 (file_info, "unix::mode"); /* Don't make setuid files on checkout when we're doing --user */ if (mode == OSTREE_REPO_CHECKOUT_MODE_USER) file_mode &= ~(S_ISUID|S_ISGID); if (TEMP_FAILURE_RETRY (fchmod (outfd, file_mode)) < 0) return glnx_throw_errno_prefix (error, "fchmod"); if (fsync_is_enabled (self, options)) { if (fsync (outfd) == -1) return glnx_throw_errno_prefix (error, "fsync"); } if (outstream) { if (!g_output_stream_close (outstream, cancellable, error)) return FALSE; } return TRUE; } /* * Create a copy of a file, supporting optional union/add behavior. */ static gboolean create_file_copy_from_input_at (OstreeRepo *repo, OstreeRepoCheckoutAtOptions *options, CheckoutState *state, GFileInfo *file_info, GVariant *xattrs, GInputStream *input, int destination_dfd, const char *destination_name, GCancellable *cancellable, GError **error) { const gboolean union_mode = options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES; const gboolean add_mode = options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_ADD_FILES; const gboolean sepolicy_enabled = options->sepolicy && !repo->disable_xattrs; g_autoptr(GVariant) modified_xattrs = NULL; /* If we're doing SELinux labeling, prepare it */ if (sepolicy_enabled) { /* If doing sepolicy path-based labeling, we don't want to set the * security.selinux attr via the generic xattr paths in either the symlink * or regfile cases, so filter it out. */ modified_xattrs = _ostree_filter_selinux_xattr (xattrs); xattrs = modified_xattrs; } if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_SYMBOLIC_LINK) { g_auto(OstreeSepolicyFsCreatecon) fscreatecon = { 0, }; if (sepolicy_enabled) { /* For symlinks, since we don't have O_TMPFILE, we use setfscreatecon() */ if (!_ostree_sepolicy_preparefscreatecon (&fscreatecon, options->sepolicy, state->selabel_path_buf->str, g_file_info_get_attribute_uint32 (file_info, "unix::mode"), error)) return FALSE; } if (symlinkat (g_file_info_get_symlink_target (file_info), destination_dfd, destination_name) < 0) { /* Handle union/add behaviors if we get EEXIST */ if (errno == EEXIST && union_mode) { /* Unioning? Let's unlink and try again */ (void) unlinkat (destination_dfd, destination_name, 0); if (symlinkat (g_file_info_get_symlink_target (file_info), destination_dfd, destination_name) < 0) return glnx_throw_errno (error); } else if (errno == EEXIST && add_mode) /* Note early return - we don't want to set the xattrs below */ return TRUE; else return glnx_throw_errno (error); } /* Process ownership and xattrs now that we made the link */ if (options->mode != OSTREE_REPO_CHECKOUT_MODE_USER) { if (TEMP_FAILURE_RETRY (fchownat (destination_dfd, destination_name, g_file_info_get_attribute_uint32 (file_info, "unix::uid"), g_file_info_get_attribute_uint32 (file_info, "unix::gid"), AT_SYMLINK_NOFOLLOW)) == -1) return glnx_throw_errno_prefix (error, "fchownat"); if (xattrs != NULL && !glnx_dfd_name_set_all_xattrs (destination_dfd, destination_name, xattrs, cancellable, error)) return FALSE; } } else if (g_file_info_get_file_type (file_info) == G_FILE_TYPE_REGULAR) { g_auto(GLnxTmpfile) tmpf = { 0, }; GLnxLinkTmpfileReplaceMode replace_mode; if (!glnx_open_tmpfile_linkable_at (destination_dfd, ".", O_WRONLY | O_CLOEXEC, &tmpf, error)) return FALSE; if (sepolicy_enabled && options->mode != OSTREE_REPO_CHECKOUT_MODE_USER) { g_autofree char *label = NULL; if (!ostree_sepolicy_get_label (options->sepolicy, state->selabel_path_buf->str, g_file_info_get_attribute_uint32 (file_info, "unix::mode"), &label, cancellable, error)) return FALSE; if (fsetxattr (tmpf.fd, "security.selinux", label, strlen (label), 0) < 0) return glnx_throw_errno_prefix (error, "Setting security.selinux"); } if (!write_regular_file_content (repo, options, tmpf.fd, file_info, xattrs, input, cancellable, error)) return FALSE; /* The add/union/none behaviors map directly to GLnxLinkTmpfileReplaceMode */ if (add_mode) replace_mode = GLNX_LINK_TMPFILE_NOREPLACE_IGNORE_EXIST; else if (union_mode) replace_mode = GLNX_LINK_TMPFILE_REPLACE; else replace_mode = GLNX_LINK_TMPFILE_NOREPLACE; if (!glnx_link_tmpfile_at (&tmpf, replace_mode, destination_dfd, destination_name, error)) return FALSE; } else g_assert_not_reached (); return TRUE; } typedef enum { HARDLINK_RESULT_NOT_SUPPORTED, HARDLINK_RESULT_SKIP_EXISTED, HARDLINK_RESULT_LINKED } HardlinkResult; static gboolean checkout_file_hardlink (OstreeRepo *self, OstreeRepoCheckoutAtOptions *options, const char *loose_path, int destination_dfd, const char *destination_name, gboolean allow_noent, HardlinkResult *out_result, GCancellable *cancellable, GError **error) { HardlinkResult ret_result = HARDLINK_RESULT_NOT_SUPPORTED; int srcfd = _ostree_repo_mode_is_bare (self->mode) ? self->objects_dir_fd : self->uncompressed_objects_dir_fd; again: if (linkat (srcfd, loose_path, destination_dfd, destination_name, 0) == 0) ret_result = HARDLINK_RESULT_LINKED; else if (!options->no_copy_fallback && (errno == EMLINK || errno == EXDEV || errno == EPERM)) { /* EMLINK, EXDEV and EPERM shouldn't be fatal; we just can't do the * optimization of hardlinking instead of copying. */ } else if (allow_noent && errno == ENOENT) { } else if (errno == EEXIST && options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_ADD_FILES) { /* In this mode, we keep existing content. Distinguish this case though to * avoid inserting into the devino cache. */ ret_result = HARDLINK_RESULT_SKIP_EXISTED; } else if (errno == EEXIST && options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES) { /* Idiocy, from man rename(2) * * "If oldpath and newpath are existing hard links referring to * the same file, then rename() does nothing, and returns a * success status." * * So we can't make this atomic. */ (void) unlinkat (destination_dfd, destination_name, 0); goto again; } else { return glnx_throw_errno_prefix (error, "Hardlinking %s to %s", loose_path, destination_name); } if (out_result) *out_result = ret_result; return TRUE; } static gboolean checkout_one_file_at (OstreeRepo *repo, OstreeRepoCheckoutAtOptions *options, CheckoutState *state, const char *checksum, int destination_dfd, const char *destination_name, GCancellable *cancellable, GError **error) { gboolean need_copy = TRUE; gboolean is_bare_user_symlink = FALSE; char loose_path_buf[_OSTREE_LOOSE_PATH_MAX]; /* FIXME - avoid the GFileInfo here */ g_autoptr(GFileInfo) source_info = NULL; if (!ostree_repo_load_file (repo, checksum, NULL, &source_info, NULL, cancellable, error)) return FALSE; const gboolean is_symlink = (g_file_info_get_file_type (source_info) == G_FILE_TYPE_SYMBOLIC_LINK); const gboolean is_whiteout = (!is_symlink && options->process_whiteouts && g_str_has_prefix (destination_name, WHITEOUT_PREFIX)); /* First, see if it's a Docker whiteout, * https://github.com/docker/docker/blob/1a714e76a2cb9008cd19609059e9988ff1660b78/pkg/archive/whiteouts.go */ if (is_whiteout) { const char *name = destination_name + (sizeof (WHITEOUT_PREFIX) - 1); if (!name[0]) return glnx_throw (error, "Invalid empty whiteout '%s'", name); g_assert (name[0] != '/'); /* Sanity */ if (!glnx_shutil_rm_rf_at (destination_dfd, name, cancellable, error)) return FALSE; need_copy = FALSE; } else if (!options->force_copy) { HardlinkResult hardlink_res = HARDLINK_RESULT_NOT_SUPPORTED; /* Try to do a hardlink first, if it's a regular file. This also * traverses all parent repos. */ OstreeRepo *current_repo = repo; while (current_repo) { /* TODO - Hoist this up to the toplevel at least for checking out from * !parent; don't need to compute it for each file. */ gboolean repo_is_usermode = current_repo->mode == OSTREE_REPO_MODE_BARE_USER || current_repo->mode == OSTREE_REPO_MODE_BARE_USER_ONLY; /* We're hardlinkable if the checkout mode matches the repo mode */ gboolean is_hardlinkable = (current_repo->mode == OSTREE_REPO_MODE_BARE && options->mode == OSTREE_REPO_CHECKOUT_MODE_NONE) || (repo_is_usermode && options->mode == OSTREE_REPO_CHECKOUT_MODE_USER); gboolean current_can_cache = (options->enable_uncompressed_cache && current_repo->enable_uncompressed_cache); gboolean is_archive_z2_with_cache = (current_repo->mode == OSTREE_REPO_MODE_ARCHIVE_Z2 && options->mode == OSTREE_REPO_CHECKOUT_MODE_USER && current_can_cache); /* NOTE: bare-user symlinks are not stored as symlinks; see * https://github.com/ostreedev/ostree/commit/47c612e5a0688c3452a125655a245e8f4f01b2b0 * as well as write_object(). */ is_bare_user_symlink = (repo_is_usermode && is_symlink); const gboolean is_bare = is_hardlinkable && !is_bare_user_symlink; /* Verify if no_copy_fallback is set that we can hardlink, with a * special exception for bare-user symlinks. */ if (options->no_copy_fallback && !is_hardlinkable && !is_bare_user_symlink) return glnx_throw (error, repo_is_usermode ? "User repository mode requires user checkout mode to hardlink" : "Bare repository mode cannot hardlink in user checkout mode"); /* But only under these conditions */ if (is_bare || is_archive_z2_with_cache) { /* Override repo mode; for archive we're looking in the cache, which is in "bare" form */ _ostree_loose_path (loose_path_buf, checksum, OSTREE_OBJECT_TYPE_FILE, OSTREE_REPO_MODE_BARE); if (!checkout_file_hardlink (current_repo, options, loose_path_buf, destination_dfd, destination_name, TRUE, &hardlink_res, cancellable, error)) return FALSE; if (hardlink_res == HARDLINK_RESULT_LINKED && options->devino_to_csum_cache) { struct stat stbuf; OstreeDevIno *key; if (TEMP_FAILURE_RETRY (fstatat (destination_dfd, destination_name, &stbuf, AT_SYMLINK_NOFOLLOW)) != 0) return glnx_throw_errno (error); key = g_new (OstreeDevIno, 1); key->dev = stbuf.st_dev; key->ino = stbuf.st_ino; memcpy (key->checksum, checksum, OSTREE_SHA256_STRING_LEN+1); g_hash_table_add ((GHashTable*)options->devino_to_csum_cache, key); } if (hardlink_res != HARDLINK_RESULT_NOT_SUPPORTED) break; } current_repo = current_repo->parent_repo; } need_copy = (hardlink_res == HARDLINK_RESULT_NOT_SUPPORTED); } const gboolean can_cache = (options->enable_uncompressed_cache && repo->enable_uncompressed_cache); g_autoptr(GInputStream) input = NULL; g_autoptr(GVariant) xattrs = NULL; /* Ok, if we're archive and we didn't find an object, uncompress * it now, stick it in the cache, and then hardlink to that. */ if (can_cache && !is_whiteout && !is_symlink && need_copy && repo->mode == OSTREE_REPO_MODE_ARCHIVE_Z2 && options->mode == OSTREE_REPO_CHECKOUT_MODE_USER) { HardlinkResult hardlink_res = HARDLINK_RESULT_NOT_SUPPORTED; if (!ostree_repo_load_file (repo, checksum, &input, NULL, NULL, cancellable, error)) return FALSE; /* Overwrite any parent repo from earlier */ _ostree_loose_path (loose_path_buf, checksum, OSTREE_OBJECT_TYPE_FILE, OSTREE_REPO_MODE_BARE); if (!checkout_object_for_uncompressed_cache (repo, loose_path_buf, source_info, input, cancellable, error)) return glnx_prefix_error (error, "Unpacking loose object %s", checksum); g_clear_object (&input); /* Store the 2-byte objdir prefix (e.g. e3) in a set. The basic * idea here is that if we had to unpack an object, it's very * likely we're replacing some other object, so we may need a GC. * * This model ensures that we do work roughly proportional to * the size of the changes. For example, we don't scan any * directories if we didn't modify anything, meaning you can * checkout the same tree multiple times very quickly. * * This is also scale independent; we don't hardcode e.g. looking * at 1000 objects. * * The downside is that if we're unlucky, we may not free * an object for quite some time. */ g_mutex_lock (&repo->cache_lock); { gpointer key = GUINT_TO_POINTER ((g_ascii_xdigit_value (checksum[0]) << 4) + g_ascii_xdigit_value (checksum[1])); if (repo->updated_uncompressed_dirs == NULL) repo->updated_uncompressed_dirs = g_hash_table_new (NULL, NULL); g_hash_table_add (repo->updated_uncompressed_dirs, key); } g_mutex_unlock (&repo->cache_lock); if (!checkout_file_hardlink (repo, options, loose_path_buf, destination_dfd, destination_name, FALSE, &hardlink_res, cancellable, error)) return glnx_prefix_error (error, "Using new cached uncompressed hardlink of %s to %s", checksum, destination_name); need_copy = (hardlink_res == HARDLINK_RESULT_NOT_SUPPORTED); } /* Fall back to copy if we couldn't hardlink */ if (need_copy) { /* Bare user mode can't hardlink symlinks, so we need to do a copy for * those. (Although in the future we could hardlink inside checkouts) This * assertion is intended to ensure that for regular files at least, we * succeeded at hardlinking above. */ if (options->no_copy_fallback) g_assert (is_bare_user_symlink); if (!ostree_repo_load_file (repo, checksum, &input, NULL, &xattrs, cancellable, error)) return FALSE; if (!create_file_copy_from_input_at (repo, options, state, source_info, xattrs, input, destination_dfd, destination_name, cancellable, error)) return glnx_prefix_error (error, "Copy checkout of %s to %s", checksum, destination_name); if (input) { if (!g_input_stream_close (input, cancellable, error)) return FALSE; } } return TRUE; } /* * checkout_tree_at: * @self: Repo * @mode: Options controlling all files * @state: Any state we're carrying through * @overwrite_mode: Whether or not to overwrite files * @destination_parent_fd: Place tree here * @destination_name: Use this name for tree * @source: Source tree * @source_info: Source info * @cancellable: Cancellable * @error: Error * * Like ostree_repo_checkout_tree(), but check out @source into the * relative @destination_name, located by @destination_parent_fd. */ static gboolean checkout_tree_at_recurse (OstreeRepo *self, OstreeRepoCheckoutAtOptions *options, CheckoutState *state, int destination_parent_fd, const char *destination_name, const char *dirtree_checksum, const char *dirmeta_checksum, GCancellable *cancellable, GError **error) { gboolean did_exist = FALSE; const gboolean sepolicy_enabled = options->sepolicy && !self->disable_xattrs; g_autoptr(GVariant) dirtree = NULL; g_autoptr(GVariant) dirmeta = NULL; g_autoptr(GVariant) xattrs = NULL; g_autoptr(GVariant) modified_xattrs = NULL; if (!ostree_repo_load_variant (self, OSTREE_OBJECT_TYPE_DIR_TREE, dirtree_checksum, &dirtree, error)) return FALSE; if (!ostree_repo_load_variant (self, OSTREE_OBJECT_TYPE_DIR_META, dirmeta_checksum, &dirmeta, error)) return FALSE; /* Parse OSTREE_OBJECT_TYPE_DIR_META */ guint32 uid, gid, mode; g_variant_get (dirmeta, "(uuu@a(ayay))", &uid, &gid, &mode, options->mode != OSTREE_REPO_CHECKOUT_MODE_USER ? &xattrs : NULL); uid = GUINT32_FROM_BE (uid); gid = GUINT32_FROM_BE (gid); mode = GUINT32_FROM_BE (mode); /* First, make the directory. Push a new scope in case we end up using * setfscreatecon(). */ { g_auto(OstreeSepolicyFsCreatecon) fscreatecon = { 0, }; /* If we're doing SELinux labeling, prepare it */ if (sepolicy_enabled) { /* We'll set the xattr via setfscreatecon(), so don't do it via generic xattrs below. */ modified_xattrs = _ostree_filter_selinux_xattr (xattrs); xattrs = modified_xattrs; if (!_ostree_sepolicy_preparefscreatecon (&fscreatecon, options->sepolicy, state->selabel_path_buf->str, mode, error)) return FALSE; } /* Create initially with mode 0700, then chown/chmod only when we're * done. This avoids anyone else being able to operate on partially * constructed dirs. */ if (TEMP_FAILURE_RETRY (mkdirat (destination_parent_fd, destination_name, 0700)) < 0) { if (errno == EEXIST && (options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES || options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_ADD_FILES || options->overwrite_mode == OSTREE_REPO_CHECKOUT_OVERWRITE_DISJOINT_UNION_FILES)) did_exist = TRUE; else return glnx_throw_errno (error); } } glnx_fd_close int destination_dfd = -1; if (!glnx_opendirat (destination_parent_fd, destination_name, TRUE, &destination_dfd, error)) return FALSE; struct stat repo_dfd_stat; if (fstat (self->repo_dir_fd, &repo_dfd_stat) < 0) return glnx_throw_errno (error); struct stat destination_stat; if (fstat (destination_dfd, &destination_stat) < 0) return glnx_throw_errno (error); if (options->no_copy_fallback && repo_dfd_stat.st_dev != destination_stat.st_dev) return glnx_throw (error, "Unable to do hardlink checkout across devices (src=%"G_GUINT64_FORMAT" destination=%"G_GUINT64_FORMAT")", (guint64)repo_dfd_stat.st_dev, (guint64)destination_stat.st_dev); /* Set the xattrs if we created the dir */ if (!did_exist && xattrs) { if (!glnx_fd_set_all_xattrs (destination_dfd, xattrs, cancellable, error)) return FALSE; } GString *selabel_path_buf = state->selabel_path_buf; /* Process files in this subdir */ { g_autoptr(GVariant) dir_file_contents = g_variant_get_child_value (dirtree, 0); GVariantIter viter; g_variant_iter_init (&viter, dir_file_contents); const char *fname; g_autoptr(GVariant) contents_csum_v = NULL; while (g_variant_iter_loop (&viter, "(&s@ay)", &fname, &contents_csum_v)) { const size_t origlen = selabel_path_buf ? selabel_path_buf->len : 0; if (selabel_path_buf) g_string_append (selabel_path_buf, fname); char tmp_checksum[OSTREE_SHA256_STRING_LEN+1]; _ostree_checksum_inplace_from_bytes_v (contents_csum_v, tmp_checksum); if (!checkout_one_file_at (self, options, state, tmp_checksum, destination_dfd, fname, cancellable, error)) return FALSE; if (selabel_path_buf) g_string_truncate (selabel_path_buf, origlen); } contents_csum_v = NULL; /* iter_loop freed it */ } /* Process subdirectories */ { g_autoptr(GVariant) dir_subdirs = g_variant_get_child_value (dirtree, 1); const char *dname; g_autoptr(GVariant) subdirtree_csum_v = NULL; g_autoptr(GVariant) subdirmeta_csum_v = NULL; GVariantIter viter; g_variant_iter_init (&viter, dir_subdirs); while (g_variant_iter_loop (&viter, "(&s@ay@ay)", &dname, &subdirtree_csum_v, &subdirmeta_csum_v)) { const size_t origlen = selabel_path_buf ? selabel_path_buf->len : 0; if (selabel_path_buf) { g_string_append (selabel_path_buf, dname); g_string_append_c (selabel_path_buf, '/'); } char subdirtree_checksum[OSTREE_SHA256_STRING_LEN+1]; _ostree_checksum_inplace_from_bytes_v (subdirtree_csum_v, subdirtree_checksum); char subdirmeta_checksum[OSTREE_SHA256_STRING_LEN+1]; _ostree_checksum_inplace_from_bytes_v (subdirmeta_csum_v, subdirmeta_checksum); if (!checkout_tree_at_recurse (self, options, state, destination_dfd, dname, subdirtree_checksum, subdirmeta_checksum, cancellable, error)) return FALSE; if (selabel_path_buf) g_string_truncate (selabel_path_buf, origlen); } } /* We do fchmod/fchown last so that no one else could access the * partially created directory and change content we're laying out. */ if (!did_exist) { guint32 canonical_mode; /* Silently ignore world-writable directories (plus sticky, suid bits, * etc.) when doing a checkout for bare-user-only repos, or if requested explicitly. * This is related to the logic in ostree-repo-commit.c for files. * See also: https://github.com/ostreedev/ostree/pull/909 i.e. 0c4b3a2b6da950fd78e63f9afec602f6188f1ab0 */ if (self->mode == OSTREE_REPO_MODE_BARE_USER_ONLY || options->bareuseronly_dirs) canonical_mode = (mode & 0775) | S_IFDIR; else canonical_mode = mode; if (TEMP_FAILURE_RETRY (fchmod (destination_dfd, canonical_mode)) < 0) return glnx_throw_errno_prefix (error, "fchmod"); } if (!did_exist && options->mode != OSTREE_REPO_CHECKOUT_MODE_USER) { if (TEMP_FAILURE_RETRY (fchown (destination_dfd, uid, gid)) < 0) return glnx_throw_errno (error); } /* Set directory mtime to OSTREE_TIMESTAMP, so that it is constant for all checkouts. * Must be done after setting permissions and creating all children. Note we skip doing * this for directories that already exist (under the theory we possibly don't own them), * and we also skip it if doing copying checkouts, which is mostly for /etc. */ if (!did_exist && !options->force_copy) { const struct timespec times[2] = { { OSTREE_TIMESTAMP, UTIME_OMIT }, { OSTREE_TIMESTAMP, 0} }; if (TEMP_FAILURE_RETRY (futimens (destination_dfd, times)) < 0) return glnx_throw_errno (error); } if (fsync_is_enabled (self, options)) { if (fsync (destination_dfd) == -1) return glnx_throw_errno (error); } return TRUE; } /* Begin a checkout process */ static gboolean checkout_tree_at (OstreeRepo *self, OstreeRepoCheckoutAtOptions *options, int destination_parent_fd, const char *destination_name, OstreeRepoFile *source, GFileInfo *source_info, GCancellable *cancellable, GError **error) { g_auto(CheckoutState) state = { 0, }; // If SELinux labeling is enabled, we need to keep track of the full path string if (options->sepolicy) { GString *buf = g_string_new (options->sepolicy_prefix ?: options->subpath); g_assert_cmpint (buf->len, >, 0); // Ensure it ends with / if (buf->str[buf->len-1] != '/') g_string_append_c (buf, '/'); state.selabel_path_buf = buf; /* Otherwise it'd just be corrupting things, and there's no use case */ g_assert (options->force_copy); } /* Special case handling for subpath of a non-directory */ if (g_file_info_get_file_type (source_info) != G_FILE_TYPE_DIRECTORY) { /* For backwards compat reasons, we do a mkdir() here. However, as a * special case to allow callers to directly check out files without an * intermediate root directory, we will skip mkdirat() if * `destination_name` == `.`, since obviously the current directory * exists. */ int destination_dfd = destination_parent_fd; glnx_fd_close int destination_dfd_owned = -1; if (strcmp (destination_name, ".") != 0) { if (mkdirat (destination_parent_fd, destination_name, 0700) < 0 && errno != EEXIST) return glnx_throw_errno_prefix (error, "mkdirat"); if (!glnx_opendirat (destination_parent_fd, destination_name, TRUE, &destination_dfd_owned, error)) return FALSE; destination_dfd = destination_dfd_owned; } return checkout_one_file_at (self, options, &state, ostree_repo_file_get_checksum (source), destination_dfd, g_file_info_get_name (source_info), cancellable, error); } /* Cache any directory metadata we read during this operation; * see commit b7afe91e21143d7abb0adde440683a52712aa246 */ g_auto(OstreeRepoMemoryCacheRef) memcache_ref; _ostree_repo_memory_cache_ref_init (&memcache_ref, self); g_assert_cmpint (g_file_info_get_file_type (source_info), ==, G_FILE_TYPE_DIRECTORY); const char *dirtree_checksum = ostree_repo_file_tree_get_contents_checksum (source); const char *dirmeta_checksum = ostree_repo_file_tree_get_metadata_checksum (source); return checkout_tree_at_recurse (self, options, &state, destination_parent_fd, destination_name, dirtree_checksum, dirmeta_checksum, cancellable, error); } static void canonicalize_options (OstreeRepo *self, OstreeRepoCheckoutAtOptions *options) { /* Canonicalize subpath to / */ if (!options->subpath) options->subpath = "/"; /* Force USER mode for BARE_USER_ONLY always - nothing else makes sense */ if (ostree_repo_get_mode (self) == OSTREE_REPO_MODE_BARE_USER_ONLY) options->mode = OSTREE_REPO_CHECKOUT_MODE_USER; } /** * ostree_repo_checkout_tree: * @self: Repo * @mode: Options controlling all files * @overwrite_mode: Whether or not to overwrite files * @destination: Place tree here * @source: Source tree * @source_info: Source info * @cancellable: Cancellable * @error: Error * * Check out @source into @destination, which must live on the * physical filesystem. @source may be any subdirectory of a given * commit. The @mode and @overwrite_mode allow control over how the * files are checked out. */ gboolean ostree_repo_checkout_tree (OstreeRepo *self, OstreeRepoCheckoutMode mode, OstreeRepoCheckoutOverwriteMode overwrite_mode, GFile *destination, OstreeRepoFile *source, GFileInfo *source_info, GCancellable *cancellable, GError **error) { OstreeRepoCheckoutAtOptions options = { 0, }; options.mode = mode; options.overwrite_mode = overwrite_mode; /* Backwards compatibility */ options.enable_uncompressed_cache = TRUE; canonicalize_options (self, &options); return checkout_tree_at (self, &options, AT_FDCWD, gs_file_get_path_cached (destination), source, source_info, cancellable, error); } /** * ostree_repo_checkout_tree_at: (skip) * @self: Repo * @options: (allow-none): Options * @destination_dfd: Directory FD for destination * @destination_path: Directory for destination * @commit: Checksum for commit * @cancellable: Cancellable * @error: Error * * Similar to ostree_repo_checkout_tree(), but uses directory-relative * paths for the destination, uses a new `OstreeRepoCheckoutAtOptions`, * and takes a commit checksum and optional subpath pair, rather than * requiring use of `GFile` APIs for the caller. * * Note in addition that unlike ostree_repo_checkout_tree(), the * default is not to use the repository-internal uncompressed objects * cache. * * This function is deprecated. Use ostree_repo_checkout_at() instead. */ gboolean ostree_repo_checkout_tree_at (OstreeRepo *self, OstreeRepoCheckoutOptions *options, int destination_dfd, const char *destination_path, const char *commit, GCancellable *cancellable, GError **error) { OstreeRepoCheckoutAtOptions new_opts = {0, }; new_opts.mode = options->mode; new_opts.overwrite_mode = options->overwrite_mode; new_opts.enable_uncompressed_cache = options->enable_uncompressed_cache; new_opts.enable_fsync = options->disable_fsync ? FALSE : self->disable_fsync; new_opts.process_whiteouts = options->process_whiteouts; new_opts.no_copy_fallback = options->no_copy_fallback; new_opts.subpath = options->subpath; new_opts.devino_to_csum_cache = options->devino_to_csum_cache; return ostree_repo_checkout_at (self, &new_opts, destination_dfd, destination_path, commit, cancellable, error); } /** * ostree_repo_checkout_at: * @self: Repo * @options: (allow-none): Options * @destination_dfd: Directory FD for destination * @destination_path: Directory for destination * @commit: Checksum for commit * @cancellable: Cancellable * @error: Error * * Similar to ostree_repo_checkout_tree(), but uses directory-relative * paths for the destination, uses a new `OstreeRepoCheckoutAtOptions`, * and takes a commit checksum and optional subpath pair, rather than * requiring use of `GFile` APIs for the caller. * * It also replaces ostree_repo_checkout_at() which was not safe to * use with GObject introspection. * * Note in addition that unlike ostree_repo_checkout_tree(), the * default is not to use the repository-internal uncompressed objects * cache. */ gboolean ostree_repo_checkout_at (OstreeRepo *self, OstreeRepoCheckoutAtOptions *options, int destination_dfd, const char *destination_path, const char *commit, GCancellable *cancellable, GError **error) { OstreeRepoCheckoutAtOptions default_options = { 0, }; OstreeRepoCheckoutAtOptions real_options; if (!options) { default_options.subpath = NULL; options = &default_options; } /* Make a copy so we can modify the options */ real_options = *options; options = &real_options; canonicalize_options (self, options); g_return_val_if_fail (!(options->force_copy && options->no_copy_fallback), FALSE); g_return_val_if_fail (!options->sepolicy || options->force_copy, FALSE); g_autoptr(GFile) commit_root = (GFile*) _ostree_repo_file_new_for_commit (self, commit, error); if (!commit_root) return FALSE; if (!ostree_repo_file_ensure_resolved ((OstreeRepoFile*)commit_root, error)) return FALSE; g_autoptr(GFile) target_dir = NULL; if (strcmp (options->subpath, "/") != 0) target_dir = g_file_get_child (commit_root, options->subpath); else target_dir = g_object_ref (commit_root); g_autoptr(GFileInfo) target_info = g_file_query_info (target_dir, OSTREE_GIO_FAST_QUERYINFO, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, cancellable, error); if (!target_info) return FALSE; if (!checkout_tree_at (self, options, destination_dfd, destination_path, (OstreeRepoFile*)target_dir, target_info, cancellable, error)) return FALSE; return TRUE; } static guint devino_hash (gconstpointer a) { OstreeDevIno *a_i = (gpointer)a; return (guint) (a_i->dev + a_i->ino); } static int devino_equal (gconstpointer a, gconstpointer b) { OstreeDevIno *a_i = (gpointer)a; OstreeDevIno *b_i = (gpointer)b; return a_i->dev == b_i->dev && a_i->ino == b_i->ino; } /** * ostree_repo_devino_cache_new: * * OSTree has support for pairing ostree_repo_checkout_tree_at() using * hardlinks in combination with a later * ostree_repo_write_directory_to_mtree() using a (normally modified) * directory. In order for OSTree to optimally detect just the new * files, use this function and fill in the `devino_to_csum_cache` * member of `OstreeRepoCheckoutAtOptions`, then call * ostree_repo_commit_set_devino_cache(). * * Returns: (transfer full): Newly allocated cache */ OstreeRepoDevInoCache * ostree_repo_devino_cache_new (void) { return (OstreeRepoDevInoCache*) g_hash_table_new_full (devino_hash, devino_equal, g_free, NULL); } /** * ostree_repo_checkout_gc: * @self: Repo * @cancellable: Cancellable * @error: Error * * Call this after finishing a succession of checkout operations; it * will delete any currently-unused uncompressed objects from the * cache. */ gboolean ostree_repo_checkout_gc (OstreeRepo *self, GCancellable *cancellable, GError **error) { g_autoptr(GHashTable) to_clean_dirs = NULL; g_mutex_lock (&self->cache_lock); to_clean_dirs = self->updated_uncompressed_dirs; self->updated_uncompressed_dirs = g_hash_table_new (NULL, NULL); g_mutex_unlock (&self->cache_lock); if (!to_clean_dirs) return TRUE; /* Note early return */ GLNX_HASH_TABLE_FOREACH (to_clean_dirs, gpointer, prefix) { g_autofree char *objdir_name = g_strdup_printf ("%02x", GPOINTER_TO_UINT (prefix)); g_auto(GLnxDirFdIterator) dfd_iter = { 0, }; if (!glnx_dirfd_iterator_init_at (self->uncompressed_objects_dir_fd, objdir_name, FALSE, &dfd_iter, error)) return FALSE; while (TRUE) { struct dirent *dent; struct stat stbuf; if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, cancellable, error)) return FALSE; if (dent == NULL) break; if (fstatat (dfd_iter.fd, dent->d_name, &stbuf, AT_SYMLINK_NOFOLLOW) != 0) { glnx_set_error_from_errno (error); return FALSE; } if (stbuf.st_nlink == 1) { if (unlinkat (dfd_iter.fd, dent->d_name, 0) != 0) { glnx_set_error_from_errno (error); return FALSE; } } } } return TRUE; }