Merge pull request #2992 from kientzle/kientzle-acl-isint-overflow

Expand ACL testing

(cherry picked from commit b18956ea60)
This commit is contained in:
Tim Kientzle authored and Martin Matuska committed 2026-06-23 10:24:14 +02:00
1 parent d6e8e98886
commit 3741a47683
5 files changed
+1004 -17

No files matched your search

+2
View File
@@ -378,10 +378,12 @@ libarchive_test_SOURCES= \
libarchive/test/test_7zip_filename_encoding.c \
libarchive/test/test_acl_nfs4.c \
libarchive/test/test_acl_nfs4_null_id_overflow.c \
libarchive/test/test_acl_nfs4_text.c \
libarchive/test/test_acl_pax.c \
libarchive/test/test_acl_platform_nfs4.c \
libarchive/test/test_acl_platform_posix1e.c \
libarchive/test/test_acl_posix1e.c \
libarchive/test/test_acl_posix1e_text.c \
libarchive/test/test_acl_text.c \
libarchive/test/test_ar_mode.c \
libarchive/test/test_archive_api_feature.c \
+44 -17
View File
@@ -1250,11 +1250,18 @@ archive_acl_from_text_w(struct archive_acl *acl, const wchar_t *text,
type = want_type;
/* Check for a numeric ID in field n+1 or n+3. */
isint_w(field[n + 1].start, field[n + 1].end, &id);
if (isint_w(field[n + 1].start, field[n + 1].end,
&id) < 0) {
ret = ARCHIVE_WARN;
continue;
}
/* Field n+3 is optional. */
if (id == -1 && fields > n+3)
isint_w(field[n + 3].start, field[n + 3].end,
&id);
if (id == -1 && fields > n+3 &&
isint_w(field[n + 3].start, field[n + 3].end,
&id) < 0) {
ret = ARCHIVE_WARN;
continue;
}
tag = 0;
s = field[n].start;
@@ -1369,7 +1376,10 @@ archive_acl_from_text_w(struct archive_acl *acl, const wchar_t *text,
tag == ARCHIVE_ENTRY_ACL_GROUP) {
n = 1;
name = field[1];
isint_w(name.start, name.end, &id);
if (isint_w(name.start, name.end, &id) < 0) {
ret = ARCHIVE_WARN;
continue;
}
} else
n = 0;
@@ -1404,7 +1414,11 @@ archive_acl_from_text_w(struct archive_acl *acl, const wchar_t *text,
ret = ARCHIVE_WARN;
continue;
}
isint_w(field[4 + n].start, field[4 + n].end, &id);
if (isint_w(field[4 + n].start, field[4 + n].end,
&id) < 0) {
ret = ARCHIVE_WARN;
continue;
}
}
/* Add entry to the internal list. */
@@ -1438,8 +1452,8 @@ isint_w(const wchar_t *start, const wchar_t *end, int *result)
if (*start < L'0' || *start > L'9')
return (0);
if (n > (INT_MAX / 10) ||
(n == INT_MAX / 10 && (*start - L'0') > INT_MAX % 10)) {
n = INT_MAX;
(n == INT_MAX / 10 && (*start - L'0') >= INT_MAX % 10)) {
return (-1);
} else {
n *= 10;
n += *start - L'0';
@@ -1749,11 +1763,18 @@ archive_acl_from_text_nl(struct archive_acl *acl, const char *text,
type = want_type;
/* Check for a numeric ID in field n+1 or n+3. */
isint(field[n + 1].start, field[n + 1].end, &id);
if (isint(field[n + 1].start, field[n + 1].end,
&id) < 0) {
ret = ARCHIVE_WARN;
continue;
}
/* Field n+3 is optional. */
if (id == -1 && fields > (n + 3))
isint(field[n + 3].start, field[n + 3].end,
&id);
if (id == -1 && fields > (n + 3) &&
isint(field[n + 3].start, field[n + 3].end,
&id) < 0) {
ret = ARCHIVE_WARN;
continue;
}
tag = 0;
s = field[n].start;
@@ -1871,7 +1892,10 @@ archive_acl_from_text_nl(struct archive_acl *acl, const char *text,
tag == ARCHIVE_ENTRY_ACL_GROUP) {
n = 1;
name = field[1];
isint(name.start, name.end, &id);
if (isint(name.start, name.end, &id) < 0) {
ret = ARCHIVE_WARN;
continue;
}
} else
n = 0;
@@ -1906,8 +1930,11 @@ archive_acl_from_text_nl(struct archive_acl *acl, const char *text,
ret = ARCHIVE_WARN;
continue;
}
isint(field[4 + n].start, field[4 + n].end,
&id);
if (isint(field[4 + n].start, field[4 + n].end,
&id) < 0) {
ret = ARCHIVE_WARN;
continue;
}
}
/* Add entry to the internal list. */
@@ -1941,8 +1968,8 @@ isint(const char *start, const char *end, int *result)
if (*start < '0' || *start > '9')
return (0);
if (n > (INT_MAX / 10) ||
(n == INT_MAX / 10 && (*start - '0') > INT_MAX % 10)) {
n = INT_MAX;
(n == INT_MAX / 10 && (*start - '0') >= INT_MAX % 10)) {
return (-1);
} else {
n *= 10;
n += *start - '0';
+2
View File
@@ -12,10 +12,12 @@ IF(ENABLE_TEST)
test_7zip_filename_encoding.c
test_acl_nfs4.c
test_acl_nfs4_null_id_overflow.c
test_acl_nfs4_text.c
test_acl_pax.c
test_acl_platform_nfs4.c
test_acl_platform_posix1e.c
test_acl_posix1e.c
test_acl_posix1e_text.c
test_acl_text.c
test_ar_mode.c
test_archive_api_feature.c
+426
View File
@@ -0,0 +1,426 @@
/*-
* Copyright (c) 2026 Tim Kientzle
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "test.h"
#include <limits.h>
/*
* Focused tests for NFSv4 ACL text parsing and serialization.
*
* test_acl_text.c already covers basic round-trips with well-formed input.
* These tests probe areas not reached there: all four ACL types, numeric IDs,
* extra-ID field override, isint() overflow, malformed entries, compact mode,
* and the wide-char path.
*/
/* NFSv4 text in standard (no extra ID), extra-ID, and compact+extra-ID forms.
* These are the same strings used in test_acl_text.c acltext[9..11]. */
static const char *nfs4_std =
"user:user77:rw-p--a-R-c-o-:-------:allow\n"
"user:user101:-w-pdD--------:fdin---:deny\n"
"group:group78:r-----a-R-c---:------I:allow\n"
"owner@:rwxp--aARWcCo-:-------:allow\n"
"group@:rw-p--a-R-c---:-------:allow\n"
"everyone@:r-----a-R-c--s:-------:allow";
static const char *nfs4_extra_id =
"user:user77:rw-p--a-R-c-o-:-------:allow:77\n"
"user:user101:-w-pdD--------:fdin---:deny:101\n"
"group:group78:r-----a-R-c---:------I:allow:78\n"
"owner@:rwxp--aARWcCo-:-------:allow\n"
"group@:rw-p--a-R-c---:-------:allow\n"
"everyone@:r-----a-R-c--s:-------:allow";
static const char *nfs4_compact =
"user:user77:rwpaRco::allow:77\n"
"user:user101:wpdD:fdin:deny:101\n"
"group:group78:raRc:I:allow:78\n"
"owner@:rwxpaARWcCo::allow\n"
"group@:rwpaRc::allow\n"
"everyone@:raRcs::allow";
static wchar_t *
s_to_ws(const char *s)
{
size_t len = strlen(s) + 1;
wchar_t *ws = malloc(len * sizeof(wchar_t));
assert(ws != NULL);
assert(mbstowcs(ws, s, len) != (size_t)-1);
return (ws);
}
/*
* Round-trip: parse NFS4 text → serialize → compare.
* Covers standard, extra-ID, and compact+extra-ID formats,
* for both the narrow and wide serializers.
*/
DEFINE_TEST(test_acl_nfs4_text_roundtrip)
{
struct archive_entry *ae;
char *text;
wchar_t *wtext, *ws;
ssize_t len;
ae = archive_entry_new();
assert(ae != NULL);
archive_entry_set_pathname(ae, "file");
archive_entry_set_mode(ae, AE_IFREG | 0644);
/* Standard format */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae, nfs4_std,
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
text = archive_entry_acl_to_text(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_NFS4);
assertEqualString(nfs4_std, text);
free(text);
archive_entry_acl_clear(ae);
/* Extra-ID format: trailing :id must be preserved */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae, nfs4_extra_id,
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
text = archive_entry_acl_to_text(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_NFS4 | ARCHIVE_ENTRY_ACL_STYLE_EXTRA_ID);
assertEqualString(nfs4_extra_id, text);
free(text);
archive_entry_acl_clear(ae);
/* Compact format: empty perm/flag fields must round-trip */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae, nfs4_compact,
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
text = archive_entry_acl_to_text(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_NFS4 |
ARCHIVE_ENTRY_ACL_STYLE_EXTRA_ID |
ARCHIVE_ENTRY_ACL_STYLE_COMPACT);
assertEqualString(nfs4_compact, text);
free(text);
archive_entry_acl_clear(ae);
/* Wide: standard format */
ws = s_to_ws(nfs4_std);
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text_w(ae, ws,
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
wtext = archive_entry_acl_to_text_w(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_NFS4);
assertEqualWString(ws, wtext);
free(wtext);
free(ws);
archive_entry_acl_clear(ae);
/* Wide: compact+extra-ID format */
ws = s_to_ws(nfs4_compact);
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text_w(ae, ws,
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
wtext = archive_entry_acl_to_text_w(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_NFS4 |
ARCHIVE_ENTRY_ACL_STYLE_EXTRA_ID |
ARCHIVE_ENTRY_ACL_STYLE_COMPACT);
assertEqualWString(ws, wtext);
free(wtext);
free(ws);
archive_entry_acl_clear(ae);
archive_entry_free(ae);
}
/*
* All four ACL entry types: allow, deny, audit, alarm.
* test_acl_text.c only exercises allow and deny.
*/
DEFINE_TEST(test_acl_nfs4_text_types)
{
struct archive_entry *ae;
char *text;
wchar_t *wtext, *ws;
ssize_t len;
int type, permset, tag, qual;
const char *name;
static const char *audit_alarm_text =
"user:alice:rw-p--aARWcCos:-------:audit\n"
"group:staff:rw-p--aARWcCos:-------:alarm";
ae = archive_entry_new();
assert(ae != NULL);
/* Parse and verify both audit and alarm entries are stored */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae, audit_alarm_text,
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(2,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4,
&type, &permset, &tag, &qual, &name));
assertEqualInt(ARCHIVE_ENTRY_ACL_TYPE_AUDIT, type);
assertEqualString("alice", name);
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4,
&type, &permset, &tag, &qual, &name));
assertEqualInt(ARCHIVE_ENTRY_ACL_TYPE_ALARM, type);
assertEqualString("staff", name);
/* Round-trip: serialize then compare */
text = archive_entry_acl_to_text(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_NFS4);
assertEqualString(audit_alarm_text, text);
/* Verify same round-trip via wide path */
ws = s_to_ws(audit_alarm_text);
wtext = archive_entry_acl_to_text_w(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_NFS4);
assertEqualWString(ws, wtext);
free(ws);
free(wtext);
free(text);
archive_entry_free(ae);
}
/*
* Numeric IDs in the name field and the extra-ID override field.
*
* 1. "user:1000:..." — the name field is a pure number; id is set from it.
* 2. "user:name:...:42" — the trailing id field overrides any id derived
* from the name.
* 3. "user:1000:...:42" — trailing id overrides even a numeric name.
* 4. isint() overflow: a field with more than 10 digits is clamped to INT_MAX.
*/
DEFINE_TEST(test_acl_nfs4_text_numeric_id)
{
struct archive_entry *ae;
char *text;
ssize_t len;
int type, permset, tag, qual;
const char *name;
ae = archive_entry_new();
assert(ae != NULL);
/* 1. Numeric-only name → id derived from name field */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae,
"user:1000:rw-p--a-R-c-o-:-------:allow",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(1,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4,
&type, &permset, &tag, &qual, &name));
assertEqualInt(1000, qual);
assertEqualString("1000", name);
/* Without EXTRA_ID the id is carried by the name string, not :id */
text = archive_entry_acl_to_text(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_NFS4);
assertEqualString("user:1000:rw-p--a-R-c-o-:-------:allow", text);
free(text);
/* With EXTRA_ID the id is appended */
text = archive_entry_acl_to_text(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_NFS4 | ARCHIVE_ENTRY_ACL_STYLE_EXTRA_ID);
assertEqualString("user:1000:rw-p--a-R-c-o-:-------:allow:1000", text);
free(text);
archive_entry_acl_clear(ae);
/* 2. Trailing extra-ID field overrides name-derived id */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae,
"user:user77:rw-p--a-R-c-o-:-------:allow:42",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(1,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4,
&type, &permset, &tag, &qual, &name));
assertEqualInt(42, qual);
assertEqualString("user77", name);
archive_entry_acl_clear(ae);
/* 3. Trailing extra-ID also overrides a numeric name */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae,
"user:1000:rw-p--a-R-c-o-:-------:allow:42",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(1,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4,
&type, &permset, &tag, &qual, &name));
assertEqualInt(42, qual);
archive_entry_acl_clear(ae);
/* 4. isint() overflow: extra-ID field too large → entry rejected */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"user:name:rw-p--a-R-c-o-:-------:allow:99999999999",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
archive_entry_acl_clear(ae);
/* 5. INT_MAX (2147483647) in the extra-ID field → also rejected */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"user:name:rw-p--a-R-c-o-:-------:allow:2147483647",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
archive_entry_acl_clear(ae);
/* 6. INT_MAX - 1 (2147483646) is the largest accepted id */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae,
"user:name:rw-p--a-R-c-o-:-------:allow:2147483646",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(1,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4,
&type, &permset, &tag, &qual, &name));
assertEqualInt(2147483646, qual);
archive_entry_acl_clear(ae);
/* 7. Overflow in the name field (numeric name too large) → rejected */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"user:99999999999:rw-p--a-R-c-o-:-------:allow",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
archive_entry_acl_clear(ae);
archive_entry_free(ae);
}
/*
* Malformed NFS4 ACL entries — each should return ARCHIVE_WARN without
* crashing. Also verifies that valid entries before an invalid one are
* still stored.
*/
DEFINE_TEST(test_acl_nfs4_text_invalid)
{
struct archive_entry *ae;
int type, permset, tag, qual;
const char *name;
ae = archive_entry_new();
assert(ae != NULL);
/* Unknown tag */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"bogus:name:rw-p--a-R-c-o-:-------:allow",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
archive_entry_acl_clear(ae);
/* Unknown type string */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"user:name:rw-p--a-R-c-o-:-------:bogus",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
archive_entry_acl_clear(ae);
/* Invalid character in perms field */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"user:name:rq-p--a-R-c-o-:-------:allow",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
archive_entry_acl_clear(ae);
/* Invalid character in flags field */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"user:name:rw-p--a-R-c-o-:q------:allow",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
archive_entry_acl_clear(ae);
/* Just a tag with no colon-separated fields */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae, "user",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
archive_entry_acl_clear(ae);
/* Valid entry followed by an invalid one: valid entry must be stored
* and ARCHIVE_WARN returned (not ARCHIVE_FATAL). */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"user:alice:rw-p--a-R-c-o-:-------:allow\n"
"bogus:bob:rw-p--a-R-c-o-:-------:deny",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(1,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4,
&type, &permset, &tag, &qual, &name));
assertEqualInt(ARCHIVE_ENTRY_ACL_TYPE_ALLOW, type);
assertEqualString("alice", name);
archive_entry_acl_clear(ae);
/* Same tests via the wide parser */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text_w(ae,
L"bogus:name:rw-p--a-R-c-o-:-------:allow",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
archive_entry_acl_clear(ae);
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text_w(ae,
L"user:name:rw-p--a-R-c-o-:-------:bogus",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
archive_entry_acl_clear(ae);
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text_w(ae,
L"user:name:rq-p--a-R-c-o-:-------:allow",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
archive_entry_acl_clear(ae);
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text_w(ae,
L"user:name:rw-p--a-R-c-o-:q------:allow",
ARCHIVE_ENTRY_ACL_TYPE_NFS4));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_NFS4));
archive_entry_acl_clear(ae);
archive_entry_free(ae);
}
+530
View File
@@ -0,0 +1,530 @@
/*-
* Copyright (c) 2026 Tim Kientzle
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "test.h"
#include <limits.h>
/*
* Focused tests for POSIX.1e ACL text parsing and serialization.
*
* test_acl_text.c covers basic POSIX.1e round-trips but does not exercise:
* the mask entry, the Solaris two-field format for mask and other, uppercase
* mode letters (R/W/X), isint() overflow on the POSIX path, and various
* invalid-input error paths. These tests fill those gaps.
*
* Note: archive_entry_acl_reset(ae, TYPE_ACCESS) always returns the count
* of extended ACL entries PLUS 3 synthesized base entries (USER_OBJ,
* GROUP_OBJ, OTHER) derived from the mode bits. Tests below account for
* this by adding 3 to the expected count and skipping those 3 entries
* before iterating over the named/mask entries of interest.
*/
/* ACCESS-only ACL including a mask entry. */
static const char *posix1e_access =
"user::rwx\n"
"group::r-x\n"
"other::r-x\n"
"user:alice:r-x\n"
"group:staff:rwx\n"
"mask::r-x";
/* Combined ACCESS + DEFAULT ACL, with mask in both sections. */
static const char *posix1e_full =
"user::rwx\n"
"group::r-x\n"
"other::r-x\n"
"user:alice:r-x\n"
"group:staff:rwx\n"
"mask::r-x\n"
"default:user::r-x\n"
"default:group::r-x\n"
"default:other::---\n"
"default:user:alice:r-x\n"
"default:group:staff:--x\n"
"default:mask::r-x";
/* posix1e_full with numeric IDs appended to named user/group entries. */
static const char *posix1e_extra_id =
"user::rwx\n"
"group::r-x\n"
"other::r-x\n"
"user:alice:r-x:77\n"
"group:staff:rwx:78\n"
"mask::r-x\n"
"default:user::r-x\n"
"default:group::r-x\n"
"default:other::---\n"
"default:user:alice:r-x:77\n"
"default:group:staff:--x:78\n"
"default:mask::r-x";
/*
* Solaris-style ACCESS ACL: mask and other use one colon (no empty name
* field between tag and permission string).
*/
static const char *posix1e_solaris =
"user::rwx\n"
"group::r-x\n"
"other:r-x\n"
"user:alice:r-x\n"
"mask:r-x";
static wchar_t *
s_to_ws(const char *s)
{
size_t len = strlen(s) + 1;
wchar_t *ws = malloc(len * sizeof(wchar_t));
assert(ws != NULL);
assert(mbstowcs(ws, s, len) != (size_t)-1);
return (ws);
}
/*
* Skip the three base entries (USER_OBJ, GROUP_OBJ, OTHER) that
* archive_entry_acl_reset() synthesises from mode bits and that
* archive_entry_acl_next() always returns first for ACCESS ACLs.
*/
static void
skip_base_entries(struct archive_entry *ae)
{
int type, permset, tag, qual;
const char *name;
int i;
for (i = 0; i < 3; i++)
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS,
&type, &permset, &tag, &qual, &name);
}
/*
* Round-trip: parse POSIX.1e text → serialize → compare.
* Covers ACCESS-only, combined ACCESS+DEFAULT, and the EXTRA_ID style,
* for both the narrow and wide serializers.
*/
DEFINE_TEST(test_acl_posix1e_text_roundtrip)
{
struct archive_entry *ae;
char *text;
wchar_t *wtext, *ws;
ssize_t len;
ae = archive_entry_new();
assert(ae != NULL);
archive_entry_set_pathname(ae, "file");
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* ACCESS-only with mask entry */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae, posix1e_access,
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
text = archive_entry_acl_to_text(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_ACCESS);
assertEqualString(posix1e_access, text);
free(text);
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* Combined ACCESS + DEFAULT — serializer auto-adds MARK_DEFAULT */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae, posix1e_full,
ARCHIVE_ENTRY_ACL_TYPE_POSIX1E));
text = archive_entry_acl_to_text(ae, &len, 0);
assertEqualString(posix1e_full, text);
free(text);
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* EXTRA_ID format */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae, posix1e_extra_id,
ARCHIVE_ENTRY_ACL_TYPE_POSIX1E));
text = archive_entry_acl_to_text(ae, &len,
ARCHIVE_ENTRY_ACL_STYLE_EXTRA_ID);
assertEqualString(posix1e_extra_id, text);
free(text);
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* Wide: ACCESS-only round-trip */
ws = s_to_ws(posix1e_access);
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text_w(ae, ws,
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
wtext = archive_entry_acl_to_text_w(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_ACCESS);
assertEqualWString(ws, wtext);
free(wtext);
free(ws);
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* Wide: combined ACCESS + DEFAULT */
ws = s_to_ws(posix1e_full);
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text_w(ae, ws,
ARCHIVE_ENTRY_ACL_TYPE_POSIX1E));
wtext = archive_entry_acl_to_text_w(ae, &len, 0);
assertEqualWString(ws, wtext);
free(wtext);
free(ws);
archive_entry_free(ae);
}
/*
* Solaris two-field format: "mask:rwx" and "other:rwx" omit the empty
* name field, so each has only two colon-separated tokens instead of three.
* The parser recognises this form; the serializer reproduces it when
* ARCHIVE_ENTRY_ACL_STYLE_SOLARIS is set.
*/
DEFINE_TEST(test_acl_posix1e_text_solaris)
{
struct archive_entry *ae;
char *text;
int type, permset, tag, qual;
const char *name;
ssize_t len;
ae = archive_entry_new();
assert(ae != NULL);
archive_entry_set_pathname(ae, "file");
archive_entry_set_mode(ae, AE_IFREG | 0755);
/*
* Parse Solaris-format text. Extended entries are: named user alice
* and mask — 2 extended + 3 base = 5 total.
*/
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae, posix1e_solaris,
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(5,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
skip_base_entries(ae);
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS,
&type, &permset, &tag, &qual, &name));
assertEqualInt(ARCHIVE_ENTRY_ACL_USER, tag);
assertEqualString("alice", name);
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS,
&type, &permset, &tag, &qual, &name));
assertEqualInt(ARCHIVE_ENTRY_ACL_MASK, tag);
assertEqualInt(ARCHIVE_ENTRY_ACL_READ | ARCHIVE_ENTRY_ACL_EXECUTE,
permset);
/* Serialize with STYLE_SOLARIS and compare */
text = archive_entry_acl_to_text(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_ACCESS | ARCHIVE_ENTRY_ACL_STYLE_SOLARIS);
assertEqualString(posix1e_solaris, text);
free(text);
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* Solaris style with non-empty name on other field → ARCHIVE_WARN */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"user::rwx\ngroup::r-x\nother:nobody:r-x\n",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
archive_entry_acl_clear(ae);
archive_entry_free(ae);
}
/*
* ismode() accepts uppercase R, W, X as well as lowercase; the serializer
* always emits lowercase. Both narrow and wide parsers share this
* behaviour.
*/
DEFINE_TEST(test_acl_posix1e_text_ismode)
{
struct archive_entry *ae;
char *text;
int type, permset, tag, qual;
const char *name;
ssize_t len;
ae = archive_entry_new();
assert(ae != NULL);
archive_entry_set_pathname(ae, "file");
archive_entry_set_mode(ae, AE_IFREG | 0755);
/*
* Uppercase R/W/X treated identically to r/w/x.
* 1 extended entry (alice) + 3 base = 4 total.
*/
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae,
"user::RWX\ngroup::R-X\nother::---\nuser:alice:RwX",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(4,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
skip_base_entries(ae);
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS,
&type, &permset, &tag, &qual, &name));
assertEqualInt(ARCHIVE_ENTRY_ACL_USER, tag);
assertEqualString("alice", name);
assertEqualInt(
ARCHIVE_ENTRY_ACL_READ | ARCHIVE_ENTRY_ACL_WRITE |
ARCHIVE_ENTRY_ACL_EXECUTE, permset);
/* Serializer always emits lowercase */
text = archive_entry_acl_to_text(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_ACCESS);
assertEqualString(
"user::rwx\ngroup::r-x\nother::---\nuser:alice:rwx", text);
free(text);
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* Same via the wide parser */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text_w(ae,
L"user::RWX\ngroup::R-X\nother::---\nuser:alice:RwX",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(4,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
skip_base_entries(ae);
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS,
&type, &permset, &tag, &qual, &name));
assertEqualInt(
ARCHIVE_ENTRY_ACL_READ | ARCHIVE_ENTRY_ACL_WRITE |
ARCHIVE_ENTRY_ACL_EXECUTE, permset);
archive_entry_free(ae);
}
/*
* Numeric IDs in the name field and the extra-ID override field,
* including the isint() overflow boundary now enforced on the POSIX path.
*/
DEFINE_TEST(test_acl_posix1e_text_numeric_id)
{
struct archive_entry *ae;
char *text;
ssize_t len;
int type, permset, tag, qual;
const char *name;
ae = archive_entry_new();
assert(ae != NULL);
archive_entry_set_pathname(ae, "file");
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* 1. Numeric-only name → id derived from name field */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae,
"user::rwx\ngroup::r-x\nother::r-x\nuser:1000:rwx",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
/* 1 extended + 3 base = 4 */
assertEqualInt(4,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
skip_base_entries(ae);
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS,
&type, &permset, &tag, &qual, &name));
assertEqualInt(1000, qual);
assertEqualString("1000", name);
/* Without EXTRA_ID the id is carried by the name string */
text = archive_entry_acl_to_text(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_ACCESS);
assert(strstr(text, "user:1000:rwx") != NULL);
free(text);
/* With EXTRA_ID the id is also appended */
text = archive_entry_acl_to_text(ae, &len,
ARCHIVE_ENTRY_ACL_TYPE_ACCESS | ARCHIVE_ENTRY_ACL_STYLE_EXTRA_ID);
assert(strstr(text, "user:1000:rwx:1000") != NULL);
free(text);
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* 2. Trailing extra-ID field overrides name-derived id */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae,
"user::rwx\ngroup::r-x\nother::r-x\nuser:alice:r-x:42",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(4,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
skip_base_entries(ae);
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS,
&type, &permset, &tag, &qual, &name));
assertEqualInt(42, qual);
assertEqualString("alice", name);
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* 3. Overflow in extra-ID field → extended entry rejected, no ext entries */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"user::rwx\ngroup::r-x\nother::r-x\nuser:alice:r-x:99999999999",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* 4. INT_MAX (2147483647) in the extra-ID field → also rejected */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"user::rwx\ngroup::r-x\nother::r-x\nuser:alice:r-x:2147483647",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* 5. INT_MAX - 1 (2147483646) is the largest accepted id */
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_from_text(ae,
"user::rwx\ngroup::r-x\nother::r-x\nuser:alice:r-x:2147483646",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(4,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
skip_base_entries(ae);
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS,
&type, &permset, &tag, &qual, &name));
assertEqualInt(2147483646, qual);
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* 6. Overflow in the name field (all-digit name too large) → rejected */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"user::rwx\ngroup::r-x\nother::r-x\nuser:99999999999:r-x",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
archive_entry_acl_clear(ae);
archive_entry_free(ae);
}
/*
* Malformed POSIX.1e ACL entries — each should return ARCHIVE_WARN without
* crashing and without storing invalid data.
*/
DEFINE_TEST(test_acl_posix1e_text_invalid)
{
struct archive_entry *ae;
int type, permset, tag, qual;
const char *name;
ae = archive_entry_new();
assert(ae != NULL);
archive_entry_set_pathname(ae, "file");
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* Invalid character in permission field */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"user:alice:rqx",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* Unknown tag */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"bogus:alice:rwx",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* NFSv4 "everyone@" tag is not recognised by the POSIX parser */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"everyone@:r-----a-R-c--s:-------:allow",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* Bare tag with no colon-separated fields */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae, "user",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/*
* Valid entry followed by invalid — valid entry must be stored.
* 1 extended (alice) + 3 base = 4 total.
*/
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text(ae,
"user::rwx\ngroup::r-x\nother::r-x\n"
"user:alice:r-x\n"
"bogus:bob:rwx",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(4,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
skip_base_entries(ae);
assertEqualInt(ARCHIVE_OK,
archive_entry_acl_next(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS,
&type, &permset, &tag, &qual, &name));
assertEqualInt(ARCHIVE_ENTRY_ACL_USER, tag);
assertEqualString("alice", name);
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* Wide parser: invalid permission character */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text_w(ae,
L"user:alice:rqx",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* Wide parser: unknown tag */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text_w(ae,
L"bogus:alice:rwx",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
archive_entry_acl_clear(ae);
archive_entry_set_mode(ae, AE_IFREG | 0755);
/* Wide parser: NFSv4 tag rejected by POSIX parser */
assertEqualInt(ARCHIVE_WARN,
archive_entry_acl_from_text_w(ae,
L"everyone@:r-----a-R-c--s:-------:allow",
ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
assertEqualInt(0,
archive_entry_acl_reset(ae, ARCHIVE_ENTRY_ACL_TYPE_ACCESS));
archive_entry_free(ae);
}