diff --git a/CMakeLists.txt b/CMakeLists.txt index 93e65e19b..b1b18b06d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -123,12 +123,14 @@ check_function_exists(fileno HAVE_FILENO) check_function_exists(fseeko HAVE_FSEEKO) check_function_exists(ftello HAVE_FTELLO) check_function_exists(getprogname HAVE_GETPROGNAME) +check_function_exists(getxattr HAVE_GETXATTR) check_function_exists(GetSecurityInfo HAVE_GETSECURITYINFO) check_symbol_exists(localtime_r time.h HAVE_LOCALTIME_R) check_symbol_exists(localtime_s time.h HAVE_LOCALTIME_S) check_function_exists(memcpy_s HAVE_MEMCPY_S) check_function_exists(random HAVE_RANDOM) check_function_exists(setmode HAVE_SETMODE) +check_function_exists(setxattr HAVE_SETXATTR) check_symbol_exists(snprintf stdio.h HAVE_SNPRINTF) check_symbol_exists(snprintf_s stdio.h HAVE_SNPRINTF_S) check_symbol_exists(strcasecmp strings.h HAVE_STRCASECMP) diff --git a/config.h.in b/config.h.in index 03cd4590f..88c05331a 100644 --- a/config.h.in +++ b/config.h.in @@ -33,6 +33,7 @@ #cmakedefine HAVE_FSEEKO #cmakedefine HAVE_FTELLO #cmakedefine HAVE_GETPROGNAME +#cmakedefine HAVE_GETXATTR #cmakedefine HAVE_GETSECURITYINFO #cmakedefine HAVE_GNUTLS #cmakedefine HAVE_LIBBZ2 @@ -44,6 +45,7 @@ #cmakedefine HAVE_MKSTEMP #cmakedefine HAVE_OPENSSL #cmakedefine HAVE_SETMODE +#cmakedefine HAVE_SETXATTR #cmakedefine HAVE_SNPRINTF #cmakedefine HAVE_SNPRINTF_S #cmakedefine HAVE_STRCASECMP diff --git a/lib/zip_source_file_stdio_named.c b/lib/zip_source_file_stdio_named.c index c1f8b4e0b..1283d0939 100644 --- a/lib/zip_source_file_stdio_named.c +++ b/lib/zip_source_file_stdio_named.c @@ -39,6 +39,10 @@ #include #include #include +#if defined(__linux__) && defined(HAVE_GETXATTR) && defined(HAVE_SETXATTR) +#define USE_ACL +#include +#endif #ifdef HAVE_UNISTD_H #include #endif @@ -391,7 +395,39 @@ static bool copy_permissions(zip_source_file_context_t *ctx) { zip_error_set(&ctx->error, ZIP_ER_RENAME, errno); return false; } - /* TODO: copy ACLs */ +#ifdef USE_ACL + static const char acl_name[] = "system.posix_acl_access"; + void *acl_data; + ssize_t acl_size; + + acl_size = getxattr(ctx->fname, acl_name, NULL, 0); + if (acl_size < 0) { + if (errno != ENODATA && errno != ENOTSUP && errno != EOPNOTSUPP) { + zip_error_set(&ctx->error, ZIP_ER_RENAME, errno); + return false; + } + } + else if (acl_size > 0) { + if ((acl_data = malloc((size_t)acl_size)) == NULL) { + zip_error_set(&ctx->error, ZIP_ER_MEMORY, 0); + return false; + } + acl_size = getxattr(ctx->fname, acl_name, acl_data, (size_t)acl_size); + if (acl_size < 0) { + int saved_errno = errno; + free(acl_data); + zip_error_set(&ctx->error, ZIP_ER_RENAME, saved_errno); + return false; + } + if (setxattr(ctx->tmpname, acl_name, acl_data, (size_t)acl_size, 0) < 0) { + int saved_errno = errno; + free(acl_data); + zip_error_set(&ctx->error, ZIP_ER_RENAME, saved_errno); + return false; + } + free(acl_data); + } +#endif if (chmod(ctx->tmpname, st.st_mode) < 0) { zip_error_set(&ctx->error, ZIP_ER_RENAME, errno); return false; diff --git a/regress/CMakeLists.txt b/regress/CMakeLists.txt index 4d667b2b0..ba9e36360 100644 --- a/regress/CMakeLists.txt +++ b/regress/CMakeLists.txt @@ -9,6 +9,10 @@ set(TEST_PROGRAMS source_seek ) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND HAVE_GETXATTR AND HAVE_SETXATTR) + list(APPEND TEST_PROGRAMS acl_preserve) +endif() + set(GETOPT_USERS fread tryopen diff --git a/regress/acl-preserve.test b/regress/acl-preserve.test new file mode 100644 index 000000000..c1fb4fa80 --- /dev/null +++ b/regress/acl-preserve.test @@ -0,0 +1,6 @@ +description preserve POSIX access ACL when replacing archive +program acl_preserve +return 0 +stdout +POSIX access ACL preserved +end-of-inline-data diff --git a/regress/programs/acl_preserve.c b/regress/programs/acl_preserve.c new file mode 100644 index 000000000..4b133775d --- /dev/null +++ b/regress/programs/acl_preserve.c @@ -0,0 +1,169 @@ +/* + acl_preserve.c -- verify that replacing an archive preserves its POSIX access ACL + SPDX-License-Identifier: BSD-3-Clause +*/ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "zip.h" + +#define ACL_EA_ACCESS "system.posix_acl_access" +#define ACL_EA_VERSION 0x0002 +#define ACL_UNDEFINED_ID UINT32_MAX + +#define ACL_USER_OBJ 0x01 +#define ACL_USER 0x02 +#define ACL_GROUP_OBJ 0x04 +#define ACL_MASK 0x10 +#define ACL_OTHER 0x20 + +#define ACL_READ 0x04 +#define ACL_WRITE 0x02 + +struct acl_header { + uint32_t version; +}; + +struct acl_entry { + uint16_t tag; + uint16_t permissions; + uint32_t id; +}; + +struct test_acl { + struct acl_header header; + struct acl_entry entries[5]; +}; + +static void set_entry(struct acl_entry *entry, uint16_t tag, uint16_t permissions, uint32_t id) { + entry->tag = htole16(tag); + entry->permissions = htole16(permissions); + entry->id = htole32(id); +} + +int main(void) { + static const char archive_name[] = "acl-preserve.zip"; + static const char contents[] = "data"; + unsigned char before[sizeof(struct test_acl)]; + unsigned char after[sizeof(struct test_acl)]; + struct test_acl acl; + zip_source_t *source = NULL; + zip_t *archive = NULL; + zip_error_t error; + ssize_t before_length; + ssize_t after_length; + int error_code; + int result = 1; + + (void)remove(archive_name); + + archive = zip_open(archive_name, ZIP_CREATE | ZIP_TRUNCATE, &error_code); + if (archive == NULL) { + fprintf(stderr, "cannot create archive: %d\n", error_code); + goto done; + } + source = zip_source_buffer(archive, contents, sizeof(contents) - 1, 0); + if (source == NULL || zip_file_add(archive, "data", source, 0) < 0) { + fprintf(stderr, "cannot add archive entry\n"); + if (source != NULL) { + zip_source_free(source); + source = NULL; + } + zip_discard(archive); + archive = NULL; + goto done; + } + source = NULL; /* owned by archive */ + if (zip_close(archive) < 0) { + fprintf(stderr, "cannot finish initial archive\n"); + zip_discard(archive); + archive = NULL; + goto done; + } + archive = NULL; + + acl.header.version = htole32(ACL_EA_VERSION); + set_entry(&acl.entries[0], ACL_USER_OBJ, ACL_READ | ACL_WRITE, ACL_UNDEFINED_ID); + set_entry(&acl.entries[1], ACL_USER, ACL_READ | ACL_WRITE, 65534); + set_entry(&acl.entries[2], ACL_GROUP_OBJ, 0, ACL_UNDEFINED_ID); + set_entry(&acl.entries[3], ACL_MASK, ACL_READ | ACL_WRITE, ACL_UNDEFINED_ID); + set_entry(&acl.entries[4], ACL_OTHER, 0, ACL_UNDEFINED_ID); + + if (setxattr(archive_name, ACL_EA_ACCESS, &acl, sizeof(acl), 0) < 0) { + if (errno == ENOTSUP || errno == EOPNOTSUPP || errno == EPERM) { + result = 77; + goto done; + } + perror("setxattr"); + goto done; + } + before_length = getxattr(archive_name, ACL_EA_ACCESS, before, sizeof(before)); + if (before_length < 0) { + perror("getxattr before"); + goto done; + } + + archive = zip_open(archive_name, 0, &error_code); + if (archive == NULL) { + fprintf(stderr, "cannot reopen archive: %d\n", error_code); + goto done; + } + if (zip_set_archive_comment(archive, "changed", 7) < 0 || zip_close(archive) < 0) { + fprintf(stderr, "cannot update archive\n"); + zip_discard(archive); + archive = NULL; + goto done; + } + archive = NULL; + + after_length = getxattr(archive_name, ACL_EA_ACCESS, after, sizeof(after)); + if (after_length != before_length || memcmp(before, after, (size_t)before_length) != 0) { + fprintf(stderr, "POSIX access ACL changed during archive replacement\n"); + goto done; + } + + zip_error_init(&error); + source = zip_source_file_create(archive_name, 0, -1, &error); + if (source == NULL) { + fprintf(stderr, "cannot create named file source: %s\n", zip_error_strerror(&error)); + zip_error_fini(&error); + goto done; + } + if (zip_source_begin_write(source) < 0 || zip_source_write(source, contents, sizeof(contents) - 1) != sizeof(contents) - 1 || zip_source_commit_write(source) < 0) { + fprintf(stderr, "cannot replace named file source: %s\n", zip_error_strerror(zip_source_error(source))); + zip_source_free(source); + source = NULL; + zip_error_fini(&error); + goto done; + } + zip_source_free(source); + source = NULL; + zip_error_fini(&error); + + after_length = getxattr(archive_name, ACL_EA_ACCESS, after, sizeof(after)); + if (after_length != before_length || memcmp(before, after, (size_t)before_length) != 0) { + fprintf(stderr, "POSIX access ACL changed during direct source replacement\n"); + goto done; + } + + puts("POSIX access ACL preserved"); + result = 0; + +done: + if (archive != NULL) { + zip_discard(archive); + } + if (source != NULL) { + zip_source_free(source); + } + (void)remove(archive_name); + return result; +}