lib/pull: Add OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES
authorColin Walters <walters@verbum.org>
Mon, 12 Jun 2017 19:06:19 +0000 (15:06 -0400)
committerAtomic Bot <atomic-devel@projectatomic.io>
Tue, 13 Jun 2017 18:44:28 +0000 (18:44 +0000)
This is an option which is intended mostly for flatpak;
see: https://github.com/flatpak/flatpak/issues/845

We're adding an option for pulling into *all*
repo modes that has an effect similar to the `bare-user-only`
change from https://github.com/ostreedev/ostree/pull/909

This way one can pull content into e.g. a root-owned `bare` repository and
ensure that there aren't any setuid or world-writable files.

Closes: #926
Approved by: alexlarsson

src/libostree/ostree-repo-pull.c
src/libostree/ostree-repo.h
src/ostree/ot-builtin-pull-local.c
src/ostree/ot-builtin-pull.c
tests/basic-test.sh
tests/pull-test.sh

index 7f6d0123a21d3df2915d359aba2a1cab900869ab..2e93f90b77f219b10c0938486689a8f287e2d9b1 100644 (file)
@@ -111,6 +111,7 @@ typedef struct {
   gboolean          is_mirror;
   gboolean          is_commit_only;
   gboolean          is_untrusted;
+  gboolean          is_bareuseronly_files;
 
   GPtrArray        *dirs;
 
@@ -556,6 +557,94 @@ pull_matches_subdir (OtPullData *pull_data,
   return FALSE;
 }
 
+/* This bit mirrors similar code in commit_loose_content_object() for the
+ * bare-user-only mode. It's opt-in though for all pulls.
+ */
+static gboolean
+validate_bareuseronly_mode (OtPullData *pull_data,
+                            const char *checksum,
+                            guint32     content_mode,
+                            GError    **error)
+{
+  if (!pull_data->is_bareuseronly_files)
+    return TRUE;
+
+  if (S_ISREG (content_mode))
+    {
+      const guint32 invalid_modebits = ((content_mode & ~S_IFMT) & ~0775);
+      if (invalid_modebits > 0)
+        return glnx_throw (error, "object %s.file: invalid mode 0%04o with bits 0%04o",
+                           checksum, content_mode, invalid_modebits);
+    }
+  else if (S_ISLNK (content_mode))
+    ; /* Nothing */
+  else
+    g_assert_not_reached ();
+
+  return TRUE;
+}
+
+/* Import a single content object in the case where
+ * we have pull_data->remote_repo_local.
+ *
+ * One important special case here is handling the
+ * OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES flag.
+ */
+static gboolean
+import_one_local_content_object (OtPullData *pull_data,
+                                 const char *checksum,
+                                 GCancellable *cancellable,
+                                 GError    **error)
+{
+  g_assert (pull_data->remote_repo_local);
+
+  const gboolean trusted = !pull_data->is_untrusted;
+  if (trusted && !pull_data->is_bareuseronly_files)
+    {
+      if (!ostree_repo_import_object_from_with_trust (pull_data->repo, pull_data->remote_repo_local,
+                                                      OSTREE_OBJECT_TYPE_FILE, checksum,
+                                                      trusted,
+                                                      cancellable, error))
+        return FALSE;
+    }
+  else
+    {
+      /* In this case we either need to validate the checksum
+       * or the file mode.
+       */
+      g_autoptr(GInputStream) content_input = NULL;
+      g_autoptr(GFileInfo) content_finfo = NULL;
+      g_autoptr(GVariant) content_xattrs = NULL;
+
+      if (!ostree_repo_load_file (pull_data->remote_repo_local, checksum,
+                                  &content_input, &content_finfo, &content_xattrs,
+                                  cancellable, error))
+        return FALSE;
+
+      if (!validate_bareuseronly_mode (pull_data, checksum,
+                                       g_file_info_get_attribute_uint32 (content_finfo, "unix::mode"),
+                                       error))
+        return FALSE;
+
+      /* Now that we've potentially validated it, convert to object stream */
+      guint64 length;
+      g_autoptr(GInputStream) object_stream = NULL;
+      if (!ostree_raw_file_to_content_stream (content_input, content_finfo,
+                                              content_xattrs, &object_stream,
+                                              &length, cancellable, error))
+        return FALSE;
+
+      g_autofree guchar *real_csum = NULL;
+      if (!ostree_repo_write_content (pull_data->repo, checksum,
+                                      object_stream, length,
+                                      &real_csum,
+                                      cancellable, error))
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
 static gboolean
 scan_dirtree_object (OtPullData   *pull_data,
                      const char   *checksum,
@@ -595,15 +684,19 @@ scan_dirtree_object (OtPullData   *pull_data,
                                    &file_is_stored, cancellable, error))
         return FALSE;
 
-      if (!file_is_stored && pull_data->remote_repo_local)
+      /* If we already have this object, move on to the next */
+      if (file_is_stored)
+        continue;
+
+      /* Is this a local repo? */
+      if (pull_data->remote_repo_local)
         {
-          if (!ostree_repo_import_object_from_with_trust (pull_data->repo, pull_data->remote_repo_local,
-                                                          OSTREE_OBJECT_TYPE_FILE, file_checksum, !pull_data->is_untrusted,
-                                                          cancellable, error))
+          if (!import_one_local_content_object (pull_data, file_checksum, cancellable, error))
             return FALSE;
         }
-      else if (!file_is_stored && !g_hash_table_lookup (pull_data->requested_content, file_checksum))
+      else if (!g_hash_table_lookup (pull_data->requested_content, file_checksum))
         {
+          /* In this case we're doing HTTP pulls */
           g_hash_table_add (pull_data->requested_content, file_checksum);
           enqueue_one_object_request (pull_data, file_checksum, OSTREE_OBJECT_TYPE_FILE, path, FALSE, FALSE);
           file_checksum = NULL;  /* Transfer ownership */
@@ -2775,6 +2868,7 @@ ostree_repo_pull_with_options (OstreeRepo             *self,
   pull_data->is_mirror = (flags & OSTREE_REPO_PULL_FLAGS_MIRROR) > 0;
   pull_data->is_commit_only = (flags & OSTREE_REPO_PULL_FLAGS_COMMIT_ONLY) > 0;
   pull_data->is_untrusted = (flags & OSTREE_REPO_PULL_FLAGS_UNTRUSTED) > 0;
+  pull_data->is_bareuseronly_files = (flags & OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES) > 0;
   pull_data->cancellable = cancellable ? g_object_ref (cancellable) : NULL;
 
   if (error)
@@ -3042,11 +3136,21 @@ ostree_repo_pull_with_options (OstreeRepo             *self,
     }
   }
 
-  /* For local pulls, default to disabling static deltas so that the
-   * exact object files are copied.
-   */
-  if (pull_data->remote_repo_local && !pull_data->require_static_deltas)
-    pull_data->disable_static_deltas = TRUE;
+  if (pull_data->remote_repo_local)
+    {
+      /* For local pulls, default to disabling static deltas so that the
+       * exact object files are copied.
+       */
+      if (!pull_data->require_static_deltas)
+        pull_data->disable_static_deltas = TRUE;
+
+    }
+  else if (pull_data->is_bareuseronly_files)
+    {
+      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                           "Can't use bareuseronly-files with non-local origin repo");
+      goto out;
+    }
 
   /* We can't use static deltas if pulling into an archive-z2 repo. */
   if (self->mode == OSTREE_REPO_MODE_ARCHIVE_Z2)
index 86bed09c40f55f36592301dfa6defeea5b705652..beacbd588ea3ab5e67fea669e53285c736820096 100644 (file)
@@ -1015,12 +1015,14 @@ gboolean ostree_repo_prune_from_reachable (OstreeRepo             *self,
  * @OSTREE_REPO_PULL_FLAGS_MIRROR: Write out refs suitable for mirrors
  * @OSTREE_REPO_PULL_FLAGS_COMMIT_ONLY: Fetch only the commit metadata
  * @OSTREE_REPO_PULL_FLAGS_UNTRUSTED: Don't trust local remote
+ * @OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES: Since 2017.7.  Reject writes of content objects with modes outside of 0775.
  */
 typedef enum {
   OSTREE_REPO_PULL_FLAGS_NONE,
   OSTREE_REPO_PULL_FLAGS_MIRROR = (1 << 0),
   OSTREE_REPO_PULL_FLAGS_COMMIT_ONLY = (1 << 1),
-  OSTREE_REPO_PULL_FLAGS_UNTRUSTED = (1 << 2)
+  OSTREE_REPO_PULL_FLAGS_UNTRUSTED = (1 << 2),
+  OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES = (1 << 3)
 } OstreeRepoPullFlags;
 
 _OSTREE_PUBLIC
index 28717b854251ddc0b939ea69ffc95d974485a2b7..b19a4c6ae089b7f17646038eaf632984aba48763 100644 (file)
@@ -33,6 +33,7 @@
 static char *opt_remote;
 static gboolean opt_disable_fsync;
 static gboolean opt_untrusted;
+static gboolean opt_bareuseronly_files;
 static gboolean opt_require_static_deltas;
 static gboolean opt_gpg_verify;
 static gboolean opt_gpg_verify_summary;
@@ -42,6 +43,7 @@ static GOptionEntry options[] = {
   { "remote", 0, 0, G_OPTION_ARG_STRING, &opt_remote, "Add REMOTE to refspec", "REMOTE" },
   { "disable-fsync", 0, 0, G_OPTION_ARG_NONE, &opt_disable_fsync, "Do not invoke fsync()", NULL },
   { "untrusted", 0, 0, G_OPTION_ARG_NONE, &opt_untrusted, "Do not trust source", NULL },
+  { "bareuseronly-files", 0, 0, G_OPTION_ARG_NONE, &opt_bareuseronly_files, "Reject regular files with mode outside of 0775 (world writable, suid, etc.)", NULL },
   { "require-static-deltas", 0, 0, G_OPTION_ARG_NONE, &opt_require_static_deltas, "Require static deltas", NULL },
   { "gpg-verify", 0, 0, G_OPTION_ARG_NONE, &opt_gpg_verify, "GPG verify commits (must specify --remote)", NULL },
   { "gpg-verify-summary", 0, 0, G_OPTION_ARG_NONE, &opt_gpg_verify_summary, "GPG verify summary (must specify --remote)", NULL },
@@ -92,6 +94,8 @@ ostree_builtin_pull_local (int argc, char **argv, GCancellable *cancellable, GEr
 
   if (opt_untrusted)
     pullflags |= OSTREE_REPO_PULL_FLAGS_UNTRUSTED;
+  if (opt_bareuseronly_files)
+    pullflags |= OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES;
 
   if (opt_disable_fsync)
     ostree_repo_set_disable_fsync (repo, TRUE);
index d88c5ee848794103628f3b1229c18d198d05e0d2..bd3ac11583a725ea32dea027cb5e3b912efa40e2 100644 (file)
@@ -34,6 +34,7 @@ static gboolean opt_dry_run;
 static gboolean opt_disable_static_deltas;
 static gboolean opt_require_static_deltas;
 static gboolean opt_untrusted;
+static gboolean opt_bareuseronly_files;
 static char** opt_subpaths;
 static char** opt_http_headers;
 static char* opt_cache_dir;
@@ -50,6 +51,7 @@ static GOptionEntry options[] = {
    { "mirror", 0, 0, G_OPTION_ARG_NONE, &opt_mirror, "Write refs suitable for a mirror", NULL },
    { "subpath", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &opt_subpaths, "Only pull the provided subpath(s)", NULL },
    { "untrusted", 0, 0, G_OPTION_ARG_NONE, &opt_untrusted, "Do not trust (local) sources", NULL },
+   { "bareuseronly-files", 0, 0, G_OPTION_ARG_NONE, &opt_bareuseronly_files, "Reject regular files with mode outside of 0775 (world writable, suid, etc.)", NULL },
    { "dry-run", 0, 0, G_OPTION_ARG_NONE, &opt_dry_run, "Only print information on what will be downloaded (requires static deltas)", NULL },
    { "depth", 0, 0, G_OPTION_ARG_INT, &opt_depth, "Traverse DEPTH parents (-1=infinite) (default: 0)", "DEPTH" },
    { "url", 0, 0, G_OPTION_ARG_STRING, &opt_url, "Pull objects from this URL instead of the one from the remote config", NULL },
@@ -167,6 +169,8 @@ ostree_builtin_pull (int argc, char **argv, GCancellable *cancellable, GError **
 
   if (opt_untrusted)
     pullflags |= OSTREE_REPO_PULL_FLAGS_UNTRUSTED;
+  if (opt_bareuseronly_files)
+    pullflags |= OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES;
 
   if (opt_dry_run && !opt_require_static_deltas)
     {
index f6aa72c2b601c229e7bc3f48027989d81ee7a194..ba051e0397e31387d5d27639e57eeb85832ba4a9 100644 (file)
@@ -19,7 +19,7 @@
 
 set -euo pipefail
 
-echo "1..$((66 + ${extra_basic_tests:-0}))"
+echo "1..$((68 + ${extra_basic_tests:-0}))"
 
 $CMD_PREFIX ostree --version > version.yaml
 python -c 'import yaml; yaml.safe_load(open("version.yaml"))'
@@ -267,6 +267,32 @@ test2_commit_relpath=/objects/${test2_commitid:0:2}/${test2_commitid:2}.commit
 assert_files_hardlinked repo/${test2_commit_relpath} repo2/${test2_commit_relpath}
 echo "ok pull-local (hardlinking metadata)"
 
+cd ${test_tmpdir}
+rm repo2 -rf && mkdir repo2
+ostree_repo_init repo2 --mode=$opposite_mode
+${CMD_PREFIX} ostree --repo=repo2 pull-local --bareuseronly-files repo test2
+${CMD_PREFIX} ostree --repo=repo2 fsck -q
+echo "ok pull-local --bareuseronly-files"
+
+# This is mostly a copy of the suid test in test-basic-user-only.sh,
+# but for the `pull --bareuseronly-files` case.
+cd ${test_tmpdir}
+rm repo-input -rf
+ostree_repo_init repo-input init --mode=archive
+cd ${test_tmpdir}
+cat > statoverride.txt <<EOF
+2048 /some-setuid
+EOF
+mkdir -p files/
+echo "a setuid file" > files/some-setuid
+chmod 0644 files/some-setuid
+$CMD_PREFIX ostree --repo=repo-input commit -b content-with-suid --statoverride=statoverride.txt --tree=dir=files
+if $CMD_PREFIX ostree pull-local --repo=repo --bareuseronly-files repo-input content-with-suid 2>err.txt; then
+    assert_not_reached "copying suid file with --bareuseronly-files worked?"
+fi
+assert_file_has_content err.txt 'object.*\.file: invalid mode.*with bits 040.*'
+echo "ok pull-local (bareuseronly files)"
+
 cd ${test_tmpdir}
 ${CMD_PREFIX} ostree --repo=repo2 checkout ${CHECKOUT_U_ARG} test2 test2-checkout-from-local-clone
 cd test2-checkout-from-local-clone
index 4a4ef069bdc79c1197d6397dba6bb1255acf96e6..f1ebca4cbafed02ccfa6f3572ae1e7c53e980ed8 100644 (file)
@@ -35,7 +35,7 @@ function verify_initial_contents() {
     assert_file_has_content baz/cow '^moo$'
 }
 
-echo "1..20"
+echo "1..21"
 
 # Try both syntaxes
 repo_init --no-gpg-verify
@@ -79,6 +79,13 @@ ${CMD_PREFIX} ostree --repo=mirrorrepo pull origin main
 ${CMD_PREFIX} ostree --repo=mirrorrepo fsck
 echo "ok pull (refuses deltas)"
 
+if ${CMD_PREFIX} ostree --repo=mirrorrepo \
+                 pull origin main --bareuseronly-files 2>err.txt; then
+    assert_not_reached "--bareuseronly-files unexpectedly succeeded"
+fi
+assert_file_has_content err.txt 'bareuseronly-files with non-local'
+echo "ok pull (refuses bareuseronly)"
+
 cd ${test_tmpdir}
 rm mirrorrepo/refs/remotes/* -rf
 ${CMD_PREFIX} ostree --repo=mirrorrepo prune --refs-only