/* * Contains code taken from Flatpak. * * Copyright © 2014-2019 Red Hat, Inc * Copyright © 2017-2020 Collabora Ltd. * * SPDX-License-Identifier: LGPL-2.1-or-later * * This program 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.1 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, see <http://www.gnu.org/licenses/>. */ #include "tree-copy.h" #include <ftw.h> #include <glib.h> #include <glib/gstdio.h> #include <gio/gio.h> #include "libglnx/libglnx.h" #include "steam-runtime-tools/glib-backports-internal.h" #include "flatpak-bwrap-private.h" #include "flatpak-utils-base-private.h" #include "flatpak-utils-private.h" /* Enabling debug logging for this is rather too verbose, so only * enable it when actively debugging this module */ #if 0 #define trace(...) g_debug (__VA_ARGS__) #else #define trace(...) do { } while (0) #endif static inline gboolean gets_usrmerged (const char *path) { while (path[0] == '/') path++; if (strcmp (path, "bin") == 0 || strcmp (path, "sbin") == 0 || g_str_has_prefix (path, "bin/") || g_str_has_prefix (path, "sbin/") || (g_str_has_prefix (path, "lib") && strcmp (path, "libexec") != 0 && !g_str_has_prefix (path, "libexec/"))) return TRUE; return FALSE; } /* nftw() doesn't have a user_data argument so we need to use a global * variable :-( */ static struct { gchar *source_root; gchar *dest_root; PvCopyFlags flags; GError *error; } nftw_data; static int copy_tree_helper (const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) { size_t len; const char *suffix; g_autofree gchar *dest = NULL; g_autofree gchar *target = NULL; GError **error = &nftw_data.error; gboolean usrmerge; g_return_val_if_fail (g_str_has_prefix (fpath, nftw_data.source_root), 1); if (strcmp (fpath, nftw_data.source_root) == 0) { if (typeflag != FTW_D) { glnx_throw (error, "\"%s\" is not a directory", fpath); return 1; } if (!glnx_shutil_mkdir_p_at (-1, nftw_data.dest_root, sb->st_mode & 07777, NULL, error)) return 1; return 0; } len = strlen (nftw_data.source_root); g_return_val_if_fail (fpath[len] == '/', 1); suffix = &fpath[len + 1]; while (suffix[0] == '/') suffix++; trace ("\"%s\": suffix=\"%s\"", fpath, suffix); /* If source_root was /path/to/source and fpath was /path/to/source/foo/bar, * then suffix is now foo/bar. */ if ((nftw_data.flags & PV_COPY_FLAGS_USRMERGE) != 0 && gets_usrmerged (suffix)) { trace ("Transforming to \"usr/%s\" for /usr merge", suffix); usrmerge = TRUE; /* /path/to/dest/usr/foo/bar */ dest = g_build_filename (nftw_data.dest_root, "usr", suffix, NULL); } else { usrmerge = FALSE; /* /path/to/dest/foo/bar */ dest = g_build_filename (nftw_data.dest_root, suffix, NULL); } switch (typeflag) { case FTW_D: trace ("Is a directory"); /* If merging /usr, replace /bin, /sbin, /lib* with symlinks like * /bin -> usr/bin */ if (usrmerge && strchr (suffix, '/') == NULL) { /* /path/to/dest/bin or similar */ g_autofree gchar *in_root = g_build_filename (nftw_data.dest_root, suffix, NULL); target = g_build_filename ("usr", suffix, NULL); if (TEMP_FAILURE_RETRY (symlink (target, in_root)) != 0) { glnx_throw_errno_prefix (error, "Unable to create symlink \"%s\" -> \"%s\"", dest, target); return 1; } /* Fall through to create usr/bin or similar too */ } if (!glnx_shutil_mkdir_p_at (-1, dest, sb->st_mode & 07777, NULL, error)) return 1; break; case FTW_SL: target = glnx_readlinkat_malloc (-1, fpath, NULL, error); if (target == NULL) return 1; trace ("Is a symlink to \"%s\"", target); if (usrmerge) { trace ("Checking for compat symlinks into /usr"); /* Ignore absolute compat symlinks /lib/foo -> /usr/lib/foo. * In this case suffix would be lib/foo. (In a Debian-based * source root, Debian Policy §10.5 says this is the only * form of compat symlink that should exist in this * direction.) */ if (g_str_has_prefix (target, "/usr/") && strcmp (target + 5, suffix) == 0) { trace ("Ignoring compat symlink \"%s\" -> \"%s\"", fpath, target); return 0; } /* Ignore relative compat symlinks /lib/foo -> ../usr/lib/foo. */ if (target[0] != '/') { g_autofree gchar *dir = g_path_get_dirname (suffix); g_autofree gchar *joined = NULL; g_autofree gchar *canon = NULL; joined = g_build_filename (dir, target, NULL); trace ("Joined: \"%s\"", joined); canon = g_canonicalize_filename (joined, "/"); trace ("Canonicalized: \"%s\"", canon); if (g_str_has_prefix (canon, "/usr/") && strcmp (canon + 5, suffix) == 0) { trace ("Ignoring compat symlink \"%s\" -> \"%s\"", fpath, target); return 0; } } } if ((nftw_data.flags & PV_COPY_FLAGS_USRMERGE) != 0 && g_str_has_prefix (suffix, "usr/") && gets_usrmerged (suffix + 4)) { trace ("Checking for compat symlinks out of /usr"); /* Ignore absolute compat symlinks /usr/lib/foo -> /lib/foo. * In this case suffix would be usr/lib/foo. (In a Debian-based * source root, Debian Policy §10.5 says this is the only * form of compat symlink that should exist in this * direction.) */ if (strcmp (suffix + 3, target) == 0) { trace ("Ignoring compat symlink \"%s\" -> \"%s\"", fpath, target); return 0; } /* Ignore relative compat symlinks * /usr/lib/foo -> ../../lib/foo. */ if (target[0] != '/') { g_autofree gchar *dir = g_path_get_dirname (suffix); g_autofree gchar *joined = NULL; g_autofree gchar *canon = NULL; joined = g_build_filename (dir, target, NULL); trace ("Joined: \"%s\"", joined); canon = g_canonicalize_filename (joined, "/"); trace ("Canonicalized: \"%s\"", canon); g_assert (canon[0] == '/'); if (strcmp (suffix + 3, canon) == 0) { trace ("Ignoring compat symlink \"%s\" -> \"%s\"", fpath, target); return 0; } } } if (TEMP_FAILURE_RETRY (symlink (target, dest)) != 0) { glnx_throw_errno_prefix (error, "Unable to create symlink \"%s\" -> \"%s\"", dest, target); return 1; } break; case FTW_F: trace ("Is a regular file"); /* Fast path: try to make a hard link. */ if (link (fpath, dest) == 0) break; /* Slow path: fall back to copying. * * This does a FICLONE or copy_file_range to get btrfs reflinks * if possible, making the copy as cheap as cp --reflink=auto. * * Rather than second-guessing which errno values would result * in link() failing but a copy succeeding, we just try it * unconditionally - the worst that can happen is that this * fails too. */ if (!glnx_file_copy_at (AT_FDCWD, fpath, sb, AT_FDCWD, dest, GLNX_FILE_COPY_OVERWRITE, NULL, error)) { glnx_prefix_error (error, "Unable to copy \"%s\" to \"%s\"", fpath, dest); return 1; } break; default: glnx_throw (&nftw_data.error, "Don't know how to handle ftw type flag %d at %s", typeflag, fpath); return 1; } return 0; } gboolean pv_cheap_tree_copy (const char *source_root, const char *dest_root, PvCopyFlags flags, GError **error) { int res; g_return_val_if_fail (source_root != NULL, FALSE); g_return_val_if_fail (dest_root != NULL, FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); /* Can't run concurrently */ g_return_val_if_fail (nftw_data.source_root == NULL, FALSE); nftw_data.source_root = flatpak_canonicalize_filename (source_root); nftw_data.dest_root = flatpak_canonicalize_filename (dest_root); nftw_data.flags = flags; nftw_data.error = NULL; res = nftw (nftw_data.source_root, copy_tree_helper, 100, FTW_PHYS); if (res == -1) { g_assert (nftw_data.error == NULL); glnx_throw_errno_prefix (error, "Unable to copy \"%s\" to \"%s\"", source_root, dest_root); } else if (res != 0) { g_propagate_error (error, g_steal_pointer (&nftw_data.error)); } g_clear_pointer (&nftw_data.source_root, g_free); g_clear_pointer (&nftw_data.dest_root, g_free); g_assert (nftw_data.error == NULL); return (res == 0); }