diff --git a/src/libostree/ostree-core-private.h b/src/libostree/ostree-core-private.h index 5a2835d5ce..e81537a6e3 100644 --- a/src/libostree/ostree-core-private.h +++ b/src/libostree/ostree-core-private.h @@ -134,6 +134,19 @@ static inline char * _ostree_get_commitpartial_path (const char *checksum) return g_strconcat ("state/", checksum, ".commitpartial", NULL); } +gboolean +_ostree_validate_bareuseronly_mode (guint32 mode, + const char *checksum, + GError **error); +static inline gboolean +_ostree_validate_bareuseronly_mode_finfo (GFileInfo *finfo, + const char *checksum, + GError **error) +{ + const guint32 content_mode = g_file_info_get_attribute_uint32 (finfo, "unix::mode"); + return _ostree_validate_bareuseronly_mode (content_mode, checksum, error); +} + gboolean _ostree_parse_delta_name (const char *delta_name, char **out_from, diff --git a/src/libostree/ostree-core.c b/src/libostree/ostree-core.c index 7d34278a96..e64cee4cad 100644 --- a/src/libostree/ostree-core.c +++ b/src/libostree/ostree-core.c @@ -1998,6 +1998,29 @@ ostree_validate_structureof_dirtree (GVariant *dirtree, return TRUE; } +/* This bit mirrors similar code in commit_loose_regfile_object() for the + * bare-user-only mode. It's opt-in though for all pulls. + */ +gboolean +_ostree_validate_bareuseronly_mode (guint32 content_mode, + const char *checksum, + GError **error) +{ + if (S_ISREG (content_mode)) + { + const guint32 invalid_modebits = ((content_mode & ~S_IFMT) & ~0775); + if (invalid_modebits > 0) + return glnx_throw (error, "Content object %s: 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; +} + static gboolean validate_stat_mode_perms (guint32 mode, GError **error) diff --git a/src/libostree/ostree-repo-commit.c b/src/libostree/ostree-repo-commit.c index e226d50029..9cfa642217 100644 --- a/src/libostree/ostree-repo-commit.c +++ b/src/libostree/ostree-repo-commit.c @@ -262,10 +262,8 @@ commit_loose_regfile_object (OstreeRepo *self, } else if (self->mode == OSTREE_REPO_MODE_BARE_USER_ONLY) { - guint32 invalid_modebits = (mode & ~S_IFMT) & ~0775; - if (invalid_modebits > 0) - return glnx_throw (error, "Invalid mode 0%04o with bits 0%04o in bare-user-only repository", - mode, invalid_modebits); + if (!_ostree_validate_bareuseronly_mode (mode, checksum, error)) + return FALSE; if (!glnx_fchmod (tmpf->fd, mode, error)) return FALSE; @@ -3167,6 +3165,250 @@ G_DEFINE_BOXED_TYPE(OstreeRepoCommitModifier, ostree_repo_commit_modifier, ostree_repo_commit_modifier_ref, ostree_repo_commit_modifier_unref); +/* Special case between bare-user and bare-user-only, + * mostly for https://github.com/flatpak/flatpak/issues/845 + * see below for any more comments. + */ +static gboolean +import_is_bareuser_only_conversion (OstreeRepo *src_repo, + OstreeRepo *dest_repo, + OstreeObjectType objtype) +{ + return src_repo->mode == OSTREE_REPO_MODE_BARE_USER + && dest_repo->mode == OSTREE_REPO_MODE_BARE_USER_ONLY + && objtype == OSTREE_OBJECT_TYPE_FILE; +} + +/* Returns TRUE if we can potentially just call link() to copy an object. */ +static gboolean +import_via_hardlink_is_possible (OstreeRepo *src_repo, + OstreeRepo *dest_repo, + OstreeObjectType objtype) +{ + /* hardlinks require the owner to match and to be on the same device */ + if (!(src_repo->owner_uid == dest_repo->owner_uid && + src_repo->device == dest_repo->device)) + return FALSE; + /* Equal modes are always compatible */ + if (src_repo->mode == dest_repo->mode) + return TRUE; + /* Metadata is identical between all modes */ + if (OSTREE_OBJECT_TYPE_IS_META (objtype)) + return TRUE; + /* And now a special case between bare-user and bare-user-only, + * mostly for https://github.com/flatpak/flatpak/issues/845 + */ + if (import_is_bareuser_only_conversion (src_repo, dest_repo, objtype)) + return TRUE; + return FALSE; +} + +/* Copy the detached metadata for commit @checksum from @source repo + * to @self. + */ +static gboolean +copy_detached_metadata (OstreeRepo *self, + OstreeRepo *source, + const char *checksum, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GVariant) detached_meta = NULL; + if (!ostree_repo_read_commit_detached_metadata (source, + checksum, &detached_meta, + cancellable, error)) + return FALSE; + + if (detached_meta) + { + if (!ostree_repo_write_commit_detached_metadata (self, + checksum, detached_meta, + cancellable, error)) + return FALSE; + } + + return TRUE; +} + +/* Try to import an object by just calling linkat(); returns + * a value in @out_was_supported if we were able to do it or not. + */ +static gboolean +import_one_object_link (OstreeRepo *self, + OstreeRepo *source, + const char *checksum, + OstreeObjectType objtype, + gboolean *out_was_supported, + GCancellable *cancellable, + GError **error) +{ + const char *errprefix = glnx_strjoina ("Importing ", checksum, ".", + ostree_object_type_to_string (objtype)); + GLNX_AUTO_PREFIX_ERROR (errprefix, error); + char loose_path_buf[_OSTREE_LOOSE_PATH_MAX]; + _ostree_loose_path (loose_path_buf, checksum, objtype, self->mode); + + /* Hardlinking between bare-user → bare-user-only is only possible for regular + * files, *not* symlinks, which in bare-user are stored as regular files. At + * this point we need to parse the file to see the difference. + */ + if (import_is_bareuser_only_conversion (source, self, objtype)) + { + struct stat stbuf; + + if (!_ostree_repo_load_file_bare (source, checksum, NULL, &stbuf, + NULL, NULL, cancellable, error)) + return FALSE; + + if (S_ISREG (stbuf.st_mode)) + { + /* This is OK, we'll drop through and try a hardlink */ + } + else if (S_ISLNK (stbuf.st_mode)) + { + /* NOTE early return */ + *out_was_supported = FALSE; + return TRUE; + } + else + g_assert_not_reached (); + } + + if (!_ostree_repo_ensure_loose_objdir_at (self->objects_dir_fd, loose_path_buf, cancellable, error)) + return FALSE; + + *out_was_supported = TRUE; + if (linkat (source->objects_dir_fd, loose_path_buf, self->objects_dir_fd, loose_path_buf, 0) != 0) + { + if (errno == EEXIST) + return TRUE; + else if (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. + */ + *out_was_supported = FALSE; + return TRUE; + } + else + return glnx_throw_errno_prefix (error, "linkat"); + } + + if (objtype == OSTREE_OBJECT_TYPE_COMMIT) + { + if (!copy_detached_metadata (self, source, checksum, cancellable, error)) + return FALSE; + } + + return TRUE; +} + +/* A version of ostree_repo_import_object_from_with_trust() + * with flags; may make this public API later. + */ +gboolean +_ostree_repo_import_object (OstreeRepo *self, + OstreeRepo *source, + OstreeObjectType objtype, + const char *checksum, + OstreeRepoImportFlags flags, + GCancellable *cancellable, + GError **error) +{ + const gboolean trusted = (flags & _OSTREE_REPO_IMPORT_FLAGS_TRUSTED) > 0; + /* Implements OSTREE_REPO_PULL_FLAGS_BAREUSERONLY_FILES which was designed for flatpak */ + const gboolean verify_bareuseronly = (flags & _OSTREE_REPO_IMPORT_FLAGS_VERIFY_BAREUSERONLY) > 0; + + /* If we need to do bareuseronly verification, let's dispense with that + * first so we don't complicate the rest of the code below. + */ + if (verify_bareuseronly && !OSTREE_OBJECT_TYPE_IS_META (objtype)) + { + g_autoptr(GFileInfo) src_finfo = NULL; + if (!ostree_repo_load_file (source, checksum, + NULL, &src_finfo, NULL, + cancellable, error)) + return FALSE; + + if (!_ostree_validate_bareuseronly_mode_finfo (src_finfo, checksum, error)) + return FALSE; + } + + /* We try to import via hardlink. If the remote is explicitly not trusted + * (i.e.) their checksums may be incorrect, we skip that. Also, we require the + * repository modes to match, as well as the owner uid (since we need to be + * able to make hardlinks). + */ + if (trusted && import_via_hardlink_is_possible (source, self, objtype)) + { + gboolean hardlink_was_supported = FALSE; + + if (!import_one_object_link (self, source, checksum, objtype, + &hardlink_was_supported, + cancellable, error)) + return FALSE; + + /* If we hardlinked, we're done! */ + if (hardlink_was_supported) + return TRUE; + } + + /* The copy path */ + + /* First, do we have the object already? */ + gboolean has_object; + if (!ostree_repo_has_object (self, objtype, checksum, &has_object, + cancellable, error)) + return FALSE; + /* If we have it, we're done */ + if (has_object) + return TRUE; + + if (OSTREE_OBJECT_TYPE_IS_META (objtype)) + { + /* Metadata object */ + g_autoptr(GVariant) variant = NULL; + + if (objtype == OSTREE_OBJECT_TYPE_COMMIT) + { + /* FIXME - cleanup detached metadata if copy below fails */ + if (!copy_detached_metadata (self, source, checksum, cancellable, error)) + return FALSE; + } + + if (!ostree_repo_load_variant (source, objtype, checksum, + &variant, error)) + return FALSE; + + g_autofree guchar *real_csum = NULL; + if (!ostree_repo_write_metadata (self, objtype, + checksum, variant, + trusted ? NULL : &real_csum, + cancellable, error)) + return FALSE; + } + else + { + /* Content object */ + guint64 length; + g_autoptr(GInputStream) object_stream = NULL; + + if (!ostree_repo_load_object_stream (source, objtype, checksum, + &object_stream, &length, + cancellable, error)) + return FALSE; + + g_autofree guchar *real_csum = NULL; + if (!ostree_repo_write_content (self, checksum, + object_stream, length, + trusted ? NULL : &real_csum, + cancellable, error)) + return FALSE; + } + + return TRUE; +} + static OstreeRepoTransactionStats * ostree_repo_transaction_stats_copy (OstreeRepoTransactionStats *stats) { diff --git a/src/libostree/ostree-repo-private.h b/src/libostree/ostree-repo-private.h index dc78569058..f5eb140e29 100644 --- a/src/libostree/ostree-repo-private.h +++ b/src/libostree/ostree-repo-private.h @@ -343,6 +343,21 @@ _ostree_repo_verify_commit_internal (OstreeRepo *self, GCancellable *cancellable, GError **error); +typedef enum { + _OSTREE_REPO_IMPORT_FLAGS_NONE, + _OSTREE_REPO_IMPORT_FLAGS_TRUSTED, + _OSTREE_REPO_IMPORT_FLAGS_VERIFY_BAREUSERONLY, +} OstreeRepoImportFlags; + +gboolean +_ostree_repo_import_object (OstreeRepo *self, + OstreeRepo *source, + OstreeObjectType objtype, + const char *checksum, + OstreeRepoImportFlags flags, + GCancellable *cancellable, + GError **error); + gboolean _ostree_repo_commit_tmpf_final (OstreeRepo *self, const char *checksum, diff --git a/src/libostree/ostree-repo-pull.c b/src/libostree/ostree-repo-pull.c index 91fa7a964a..514d2041a3 100644 --- a/src/libostree/ostree-repo-pull.c +++ b/src/libostree/ostree-repo-pull.c @@ -623,33 +623,6 @@ 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; -} - /* Synchronously import a single content object; this is used async for content, * or synchronously for metadata. @src_repo is either * pull_data->remote_repo_local or one of pull_data->localcache_repos. @@ -664,51 +637,14 @@ import_one_local_content_object_sync (OtPullData *pull_data, GCancellable *cancellable, GError **error) { - 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, src_repo, - 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 (src_repo, 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; + OstreeRepoImportFlags flags = _OSTREE_REPO_IMPORT_FLAGS_NONE; + if (!pull_data->is_untrusted) + flags |= _OSTREE_REPO_IMPORT_FLAGS_TRUSTED; + if (pull_data->is_bareuseronly_files) + flags |= _OSTREE_REPO_IMPORT_FLAGS_VERIFY_BAREUSERONLY; + return _ostree_repo_import_object (pull_data->repo, src_repo, + OSTREE_OBJECT_TYPE_FILE, checksum, + flags, cancellable, error); } typedef struct { @@ -1129,11 +1065,11 @@ content_fetch_on_complete (GObject *object, */ ot_cleanup_unlinkat (&tmp_unlinker); - if (!validate_bareuseronly_mode (pull_data, - checksum, - g_file_info_get_attribute_uint32 (file_info, "unix::mode"), - error)) - goto out; + if (pull_data->is_bareuseronly_files) + { + if (!_ostree_validate_bareuseronly_mode_finfo (file_info, checksum, error)) + goto out; + } if (!ostree_raw_file_to_content_stream (file_in, file_info, xattrs, &object_input, &length, diff --git a/src/libostree/ostree-repo.c b/src/libostree/ostree-repo.c index 0056d8056e..647840b785 100644 --- a/src/libostree/ostree-repo.c +++ b/src/libostree/ostree-repo.c @@ -3367,114 +3367,6 @@ ostree_repo_delete_object (OstreeRepo *self, return TRUE; } -static gboolean -copy_detached_metadata (OstreeRepo *self, - OstreeRepo *source, - const char *checksum, - GCancellable *cancellable, - GError **error) -{ - g_autoptr(GVariant) detached_meta = NULL; - if (!ostree_repo_read_commit_detached_metadata (source, - checksum, &detached_meta, - cancellable, error)) - return FALSE; - - if (detached_meta) - { - if (!ostree_repo_write_commit_detached_metadata (self, - checksum, detached_meta, - cancellable, error)) - return FALSE; - } - - return TRUE; -} - -/* Special case between bare-user and bare-user-only, - * mostly for https://github.com/flatpak/flatpak/issues/845 - * see below for any more comments. - */ -static gboolean -import_is_bareuser_only_conversion (OstreeRepo *src_repo, - OstreeRepo *dest_repo, - OstreeObjectType objtype) -{ - return src_repo->mode == OSTREE_REPO_MODE_BARE_USER - && dest_repo->mode == OSTREE_REPO_MODE_BARE_USER_ONLY - && objtype == OSTREE_OBJECT_TYPE_FILE; -} - -static gboolean -import_one_object_link (OstreeRepo *self, - OstreeRepo *source, - const char *checksum, - OstreeObjectType objtype, - gboolean *out_was_supported, - GCancellable *cancellable, - GError **error) -{ - const char *errprefix = glnx_strjoina ("Importing ", checksum, ".", - ostree_object_type_to_string (objtype)); - GLNX_AUTO_PREFIX_ERROR (errprefix, error); - char loose_path_buf[_OSTREE_LOOSE_PATH_MAX]; - _ostree_loose_path (loose_path_buf, checksum, objtype, self->mode); - - /* Hardlinking between bare-user → bare-user-only is only possible for regular - * files, *not* symlinks, which in bare-user are stored as regular files. At - * this point we need to parse the file to see the difference. - */ - if (import_is_bareuser_only_conversion (source, self, objtype)) - { - struct stat stbuf; - - if (!_ostree_repo_load_file_bare (source, checksum, NULL, &stbuf, - NULL, NULL, cancellable, error)) - return FALSE; - - if (S_ISREG (stbuf.st_mode)) - { - /* This is OK, we'll drop through and try a hardlink */ - } - else if (S_ISLNK (stbuf.st_mode)) - { - /* NOTE early return */ - *out_was_supported = FALSE; - return TRUE; - } - else - g_assert_not_reached (); - } - - if (!_ostree_repo_ensure_loose_objdir_at (self->objects_dir_fd, loose_path_buf, cancellable, error)) - return FALSE; - - *out_was_supported = TRUE; - if (linkat (source->objects_dir_fd, loose_path_buf, self->objects_dir_fd, loose_path_buf, 0) != 0) - { - if (errno == EEXIST) - return TRUE; - else if (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. - */ - *out_was_supported = FALSE; - return TRUE; - } - else - return glnx_throw_errno_prefix (error, "linkat"); - } - - if (objtype == OSTREE_OBJECT_TYPE_COMMIT) - { - if (!copy_detached_metadata (self, source, checksum, cancellable, error)) - return FALSE; - } - - return TRUE; -} - /** * ostree_repo_import_object_from: * @self: Destination repo @@ -3504,28 +3396,6 @@ ostree_repo_import_object_from (OstreeRepo *self, checksum, TRUE, cancellable, error); } -static gboolean -import_via_hardlink_is_possible (OstreeRepo *src_repo, - OstreeRepo *dest_repo, - OstreeObjectType objtype) -{ - /* We need the ability to make hardlinks */ - if (src_repo->owner_uid != dest_repo->owner_uid) - return FALSE; - /* Equal modes are always compatible */ - if (src_repo->mode == dest_repo->mode) - return TRUE; - /* Metadata is identical between all modes */ - if (OSTREE_OBJECT_TYPE_IS_META (objtype)) - return TRUE; - /* And now a special case between bare-user and bare-user-only, - * mostly for https://github.com/flatpak/flatpak/issues/845 - */ - if (import_is_bareuser_only_conversion (src_repo, dest_repo, objtype)) - return TRUE; - return FALSE; -} - /** * ostree_repo_import_object_from_with_trust: * @self: Destination repo @@ -3552,82 +3422,12 @@ ostree_repo_import_object_from_with_trust (OstreeRepo *self, GCancellable *cancellable, GError **error) { - /* We try to import via hardlink. If the remote is explicitly not trusted - * (i.e.) their checksums may be incorrect, we skip that. Also, we require the - * repository modes to match, as well as the owner uid (since we need to be - * able to make hardlinks). - */ - if (trusted && import_via_hardlink_is_possible (source, self, objtype)) - { - gboolean hardlink_was_supported = FALSE; - - if (!import_one_object_link (self, source, checksum, objtype, - &hardlink_was_supported, - cancellable, error)) - return FALSE; - - /* If we hardlinked, we're done! */ - if (hardlink_was_supported) - return TRUE; - } - - /* The copy path */ - - /* First, do we have the object already? */ - gboolean has_object; - if (!ostree_repo_has_object (self, objtype, checksum, &has_object, - cancellable, error)) - return FALSE; - /* If we have it, we're done */ - if (has_object) - return TRUE; - - if (OSTREE_OBJECT_TYPE_IS_META (objtype)) - { - /* Metadata object */ - g_autoptr(GVariant) variant = NULL; - - if (objtype == OSTREE_OBJECT_TYPE_COMMIT) - { - /* FIXME - cleanup detached metadata if copy below fails */ - if (!copy_detached_metadata (self, source, checksum, cancellable, error)) - return FALSE; - } - - if (!ostree_repo_load_variant (source, objtype, checksum, - &variant, error)) - return FALSE; - - g_autofree guchar *real_csum = NULL; - if (!ostree_repo_write_metadata (self, objtype, - checksum, variant, - trusted ? NULL : &real_csum, - cancellable, error)) - return FALSE; - } - else - { - /* Content object */ - guint64 length; - g_autoptr(GInputStream) object_stream = NULL; - - if (!ostree_repo_load_object_stream (source, objtype, checksum, - &object_stream, &length, - cancellable, error)) - return FALSE; - - g_autofree guchar *real_csum = NULL; - if (!ostree_repo_write_content (self, checksum, - object_stream, length, - trusted ? NULL : &real_csum, - cancellable, error)) - return FALSE; - } - - return TRUE; + /* This just wraps a currently internal API, may make it public later */ + OstreeRepoImportFlags flags = trusted ? _OSTREE_REPO_IMPORT_FLAGS_TRUSTED : 0; + return _ostree_repo_import_object (self, source, objtype, checksum, + flags, cancellable, error); } - /** * ostree_repo_query_object_storage_size: * @self: Repo diff --git a/tests/basic-test.sh b/tests/basic-test.sh index 5058af1d25..50ba486b9f 100644 --- a/tests/basic-test.sh +++ b/tests/basic-test.sh @@ -314,7 +314,7 @@ $CMD_PREFIX ostree --repo=repo-input commit -b content-with-suid --statoverride= 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.*' +assert_file_has_content err.txt 'Content object.*: invalid mode.*with bits 040.*' echo "ok pull-local (bareuseronly files)" if ! skip_one_without_user_xattrs; then diff --git a/tests/pull-test.sh b/tests/pull-test.sh index f44c2cedd9..3780d1ad57 100644 --- a/tests/pull-test.sh +++ b/tests/pull-test.sh @@ -127,7 +127,7 @@ for flag in "" "--mirror"; do if ${CMD_PREFIX} ostree --repo=mirrorrepo pull ${flag} --bareuseronly-files origin content-with-suid 2>err.txt; then assert_not_reached "pulled unsafe bareuseronly" fi - assert_file_has_content err.txt 'object.*\.file: invalid mode.*with bits 040.*' + assert_file_has_content err.txt 'Content object.*: invalid mode.*with bits 040.*' done echo "ok pull (bareuseronly, unsafe)" diff --git a/tests/test-basic-user-only.sh b/tests/test-basic-user-only.sh index ce69e49e8e..19262b7b6e 100755 --- a/tests/test-basic-user-only.sh +++ b/tests/test-basic-user-only.sh @@ -45,7 +45,7 @@ $CMD_PREFIX ostree --repo=repo-input commit -b content-with-suid --statoverride= if $CMD_PREFIX ostree pull-local --repo=repo repo-input 2>err.txt; then assert_not_reached "copying suid file into bare-user worked?" fi -assert_file_has_content err.txt "Invalid mode.*with bits 040.*in bare-user-only" +assert_file_has_content err.txt "Content object.*invalid mode.*with bits 040.*" echo "ok failed to commit suid" cd ${test_tmpdir}