Skip to content

Commit bf8c8e2

Browse files
committed
Add fnmatch globbing to path rules
Rules could only compare exe and path attributes exactly, so policies could not describe stable layouts containing per-user or versioned path components. Track whether immutable string sets contain glob metacharacters, preserve ex act AVL lookup as the fast path, and evaluate exe and path patterns with pathnam e and leading-period boundaries. Concrete event paths and trust database keys re main unchanged. Add rule and attribute-set coverage for wildcard semantics, exact compatibil ity, named sets, ordering, and path immutability. Document safe use, trust requi rements, CLI behavior, and unsupported recursive globbing.
1 parent 04fda72 commit bf8c8e2

10 files changed

Lines changed: 406 additions & 15 deletions

File tree

ChangeLog

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Make fagenrules follow the generational update pattern
77
- Fix Debian backend to work again
88
- Add filter support for Debian backend
9+
- Add fnmatch globbing for exe and path rule attributes
910

1011
1.6
1112
- Many internal changes

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,17 @@ allow, use the macro set support as illustrated in this last example. This puts
224224
the list into a sorted AVL tree so that searching is cut to a minimum number
225225
of compares.
226226

227+
The `exe` and `path` rule attributes also accept `fnmatch` glob patterns for
228+
paths with a necessary variable component, such as
229+
`/opt/vendor/product-*/bin/tool`. Wildcards do not cross `/` or implicitly
230+
match a leading period, and `**` recursive matching is not supported. Continue
231+
to use `dir` for a complete subtree under a fixed directory and exact values or
232+
sets when the paths are known. A `dir` value containing glob metacharacters is
233+
a rule error reported by `fapolicyd-cli --check-rules`; use subject `exe` or
234+
object `path` when glob matching is required.
235+
236+
A glob selects matching paths but does not trust them. In particular, it is not advisable to allow execution from a user-writable path solely because its name matches a glob. It is strongly suggested to pair an object `path` allow with object `trust=1` or an explicit `FILE_HASH`; pair a subject `exe` glob in a user-writable location with subject `trust=1`. The trust database continues to contain concrete paths; see the `fapolicyd.rules(5)` man page for the complete globbing contract.
237+
227238
One last note, the rule engine is a first match wins system. If you are adding
228239
rules to allow something but it gets denied by a rule higher up, then move
229240
your rule above the thing that denies it. But again, if you are writing rules

TODO

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
Userspace
22
=========
3-
* Allow rules to express paths using globbing (fnmatch)
43
* Don't overwrite existing timer reports if the timer is not armed
54
* Improve reconfigure via SIGHUP to update configuration
65
* Consider adding rule testing to cli (uid, pgm, file)
76

87
* Support other packaging manifests
9-

doc/fapolicyd-cli.8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ matching the \fB%languages\fP macro. A non-zero status is returned when risky
7676
entries are found so automated workflows can gate changes.
7777
.TP
7878
.B \-\-check-rules [path]
79-
Parse the rules file with the daemon policy parser without loading it into the daemon. If \fIpath\fP is omitted, the command checks the active rules file, using \fI/etc/fapolicyd/fapolicyd.rules\fP when present and \fI/etc/fapolicyd/compiled.rules\fP otherwise. If both installed rules files exist, the command reports a path configuration error unless \fB\-\-lint\fP is also used. A zero exit status means the file is valid.
79+
Parse the rules file with the daemon policy parser without loading it into the daemon. If \fIpath\fP is omitted, the command checks the active rules file, using \fI/etc/fapolicyd/fapolicyd.rules\fP when present and \fI/etc/fapolicyd/compiled.rules\fP otherwise. If both installed rules files exist, the command reports a path configuration error unless \fB\-\-lint\fP is also used. A zero exit status means the file is valid. Glob values for \fBexe\fP and \fBpath\fP are accepted as rule syntax, but this command does not expand them or simulate which concrete paths they match. Glob metacharacters in \fBdir\fP values are rejected.
8080
.TP
8181
.B \-\-lint
8282
When used with \fB\-\-check-rules\fP, also emit policy-shape warnings for executable or programmatic content that can reach the default-allow path. The option may be placed before or after \fB\-\-check-rules\fP. A warning causes a non-zero exit status.

doc/fapolicyd.rules.5

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ This is a boolean describing whether it is required for the subject to be in the
5959
This is the shortened command name. When an interpreter starts a program, it usually renames the program to the script rather than the interpreter.
6060
.TP
6161
.B exe
62-
This is the full path to the executable. Globbing is not supported. You may also use the special keyword \fBuntrusted\fP to match on the subject not being listed in the rpm database.
62+
This is the full path to the executable. Exact paths and glob patterns are
63+
supported. See
64+
.B PATH GLOBBING
65+
below. You may also use the special keyword \fBuntrusted\fP to match on the
66+
subject not being listed in the rpm database.
6367
.TP
6468
.B dir
6569
If you wish to match a directory, then use this by giving the full path to the directory. Its recommended to end with the / to ensure it matches a directory. There are 3 keywords that \fIdir\fP supports: \fBexecdirs\fP, \fBsystemdirs\fP, \fBuntrusted\fP.
@@ -129,7 +133,11 @@ The object is the file that the subject is interacting with. The fields in the r
129133
This matches against any obbject. When used, this must be the only object in the rule.
130134
.TP
131135
.B path
132-
This is the full path to the file that will be accessed. Globbing is not supported. You may also use the special keyword \fBuntrusted\fP to match on the object not being listed in the rpm database.
136+
This is the full path to the file that will be accessed. Exact paths and glob
137+
patterns are supported. See
138+
.B PATH GLOBBING
139+
below. You may also use the special keyword \fBuntrusted\fP to match on the
140+
object not being listed in the rpm database.
133141
.TP
134142
.B dir
135143
If you wish to match on access to any file in a directory, then use this by giving the full path to the directory. Its recommended to end with the / to ensure it matches a directory. There are 3 keywords that \fIdir\fP supports: \fBexecdirs\fP, \fBsystemdirs\fP, \fBuntrusted\fP. See the \fBdir\fP option under Subject for an explanation of these keywords.
@@ -150,6 +158,97 @@ is still accepted)
150158
This option matches against the hash of the file being accessed. The accepted hash length follows the trust source, so RPM entries built with SHA512 store SHA512 digests while other sources may continue to provide SHA256. The hash in the rules should be all lowercase letters and do NOT start with 0x. Lowercase is the default output of sha256sum and sha512sum. The SHA256HASH keyword remains for compatibility but is deprecated; prefer FILE_HASH for new rules.
151159
.RE
152160

161+
.SH PATH GLOBBING
162+
Glob patterns are supported only by the subject
163+
.B exe
164+
and object
165+
.B path
166+
attributes. The
167+
.B dir
168+
attribute remains a literal directory prefix; use it instead of a glob when a
169+
rule applies to a complete subtree rooted at a fixed directory. A
170+
.B dir
171+
value containing a glob metacharacter is rejected, including when the value
172+
comes from a named set. Use subject
173+
.B exe
174+
or object
175+
.B path
176+
when glob matching is required.
177+
178+
Patterns use
179+
.BR fnmatch (3)
180+
syntax. A
181+
.B *
182+
matches zero or more characters,
183+
.B ?
184+
matches one character, and bracket expressions such as
185+
.B [0-9]
186+
match one character from the expression. Matching is case-sensitive and must
187+
cover the complete path. Wildcards do not match a slash, so each variable path
188+
component must be written explicitly. A wildcard does not match a leading
189+
period in a path component unless the period appears explicitly in the
190+
pattern. Backslash quotes a metacharacter. Recursive
191+
.B **
192+
globstar matching is not supported.
193+
194+
Bracket ranges such as
195+
.B [a-z]
196+
and named character classes such as
197+
.B [[:alpha:]]
198+
use the locale active in the daemon. Avoid ranges and named classes when policy
199+
must produce identical matches in every locale; use exact alternatives or
200+
enumerate the intended characters instead.
201+
202+
For example,
203+
.B /home/*/bin/tool
204+
matches
205+
.B /home/alice/bin/tool
206+
but not
207+
.B /home/alice/project/bin/tool
208+
or
209+
.BR /home/.admin/bin/tool .
210+
Glob values may appear in comma-separated attribute values and in named string
211+
sets. Rule order is unchanged: rules are evaluated from top to bottom, and the
212+
first complete rule match determines the decision.
213+
214+
The concrete object
215+
.B path
216+
is obtained from the event file descriptor through
217+
.IR /proc/self/fd/ ,
218+
and the subject
219+
.B exe
220+
path is obtained through
221+
.IR /proc/PID/exe .
222+
Symbolic links have therefore already been followed, so a rule normally matches
223+
the reported target path rather than the symbolic link name used by the
224+
application. Hard links and mount aliases can expose different concrete paths
225+
for the same inode; policy that must cover those aliases needs an exact value or
226+
pattern for each reported path.
227+
228+
Globbing selects paths; it does not expand the pattern, alter the path reported
229+
in logs, or make matching files trusted. Trust database entries continue to use
230+
concrete paths. An allow rule for a user-writable location should normally pair
231+
an object
232+
.B path
233+
glob with object
234+
.B trust=1
235+
or an explicit
236+
.B FILE_HASH
237+
constraint. A subject
238+
.B exe
239+
glob in a user-writable location should normally require subject
240+
.BR trust=1 .
241+
Otherwise, any file whose name fits the pattern can receive the allow decision.
242+
Prefer exact paths, named sets of exact paths, or
243+
.B dir
244+
when they express the policy because exact and prefix matching are less
245+
expensive than glob evaluation.
246+
247+
.nf
248+
.B deny_audit perm=execute all : path=/opt/vendor/product-*/bin/tool
249+
.B allow perm=execute all : path=/home/*/bin/tool trust=1
250+
.fi
251+
153252
.SH SETS
154253
Set is a named group of values of the same type. Fapolicyd internally distinguishes between INT and STRING set types. You can define your own set and use it as a value for a specific rule attribute. The definition is in key=value syntax and starts with a set name. The set name has to start with '%' and the rest is alphanumeric or '_'. The value is a comma separated list. The set type is inherited from the first item in the list. If that can be turned into number then whole list is expected to carry numbers. One can use these sets as a value for subject and object attributes. It is also possible to use a plain list as an attribute value without previous definition. The assigned set has to match the attribute type. It is not possible set groups for TRUST and PATTERN attributes.
155254

@@ -202,6 +301,7 @@ The following rules illustrate the rule syntax.
202301
.BR fapolicyd (8),
203302
.B fagenrules (8),
204303
.BR fapolicyd-cli (8),
304+
.BR fnmatch (3),
205305
and
206306
.BR fapolicyd.conf (5)
207307

src/library/attr-sets.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,8 @@ int attr_set_append_str(attr_sets_entry_t *set, const char *str)
362362
free(data);
363363
return 1;
364364
}
365+
if (strpbrk(str, "*?["))
366+
set->has_glob = true;
365367

366368
return 0;
367369
}

src/library/attr-sets.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ typedef struct attr_sets_entry {
4848
char * name;
4949
// STRING, SIGNED, or UNSIGNED from DATA_TYPES
5050
int type;
51+
// Lookup hint only; rule attributes decide whether globs are meaningful.
52+
bool has_glob;
5153
avl_tree_t tree;
5254
} attr_sets_entry_t;
5355

src/library/rules.c

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include <grp.h>
3333
#include <ctype.h>
3434
#include <stdint.h>
35+
#include <fnmatch.h>
3536

3637
#include "attr-sets.h"
3738
#include "policy.h"
@@ -71,6 +72,35 @@ static int assign_subject(llist *l, lnode *n, int type,
7172
static int assign_object(llist *l, lnode *n, int type,
7273
const char *ptr2, int lineno) __wur;
7374

75+
/*
76+
* validate_dir_set - reject glob patterns on literal directory attributes
77+
* @type: parsed subject or object attribute type.
78+
* @set: parsed attribute value set.
79+
* @lineno: rule line number used for diagnostics.
80+
* @side: rule side used to make the diagnostic actionable.
81+
*
82+
* Directory attributes use prefix matching rather than fnmatch. Rejecting
83+
* metacharacters prevents an accepted rule from silently matching a literal
84+
* asterisk, question mark, or bracket instead of the intended path pattern.
85+
*
86+
* Returns: 0 when the set is valid for the attribute, otherwise 1.
87+
*/
88+
static int validate_dir_set(int type, const attr_sets_entry_t *set,
89+
int lineno, const char *side)
90+
{
91+
const char *alternative;
92+
93+
if ((type != EXE_DIR && type != ODIR) || !set || !set->has_glob)
94+
return 0;
95+
96+
alternative = type == EXE_DIR ? "exe" : "path";
97+
msg(LOG_ERR,
98+
"rules: line:%d: %s dir does not support glob patterns; "
99+
"use %s for glob matching",
100+
lineno, side, alternative);
101+
return 1;
102+
}
103+
74104
/*
75105
* warn_deprecated_untrusted_dir - warn about the legacy dir=untrusted macro
76106
* @type: parsed subject or object attribute type.
@@ -594,6 +624,8 @@ static int assign_subject(llist *l, lnode *n, int type,
594624
} // switch
595625

596626
finalize:
627+
if (validate_dir_set(type, n->s[i].set, lineno, "subject"))
628+
goto free_and_error;
597629
warn_deprecated_untrusted_dir(type, n->s[i].set, lineno, "subject");
598630
n->s_count++;
599631
free(tmp);
@@ -745,6 +777,8 @@ static int assign_object(llist *l, lnode *n, int type,
745777

746778

747779
finalize:
780+
if (validate_dir_set(type, n->o[i].set, lineno, "object"))
781+
goto free_and_error;
748782
warn_deprecated_untrusted_dir(type, n->o[i].set, lineno, "object");
749783
n->o_count++;
750784
free(tmp);
@@ -1178,6 +1212,39 @@ static int check_dirs(unsigned int i, const char *path)
11781212
return 0;
11791213
}
11801214

1215+
/*
1216+
* path_set_match - match one concrete path against a rule string set
1217+
* @set: string set assigned to an exe or path rule attribute.
1218+
* @path: concrete executable or object path from the event.
1219+
*
1220+
* Exact lookup remains first so ordinary rules retain AVL lookup behavior and
1221+
* literal filenames containing metacharacters still match themselves. Globs
1222+
* are pathname-component scoped, case-sensitive, and require leading periods
1223+
* to be explicit. The event path is never changed, so trust lookup and logging
1224+
* continue to use the concrete path.
1225+
*
1226+
* Returns: 1 when an exact value or glob matches, otherwise 0.
1227+
*/
1228+
static int path_set_match(attr_sets_entry_t *set, const char *path)
1229+
{
1230+
avl_iterator iterator;
1231+
avl_str_data_t *entry;
1232+
1233+
if (attr_set_check_str(set, path))
1234+
return 1;
1235+
if (!set || set->type != STRING || !set->has_glob || !path)
1236+
return 0;
1237+
1238+
for (entry = (avl_str_data_t *)avl_first(&iterator, &set->tree);
1239+
entry; entry = (avl_str_data_t *)avl_next(&iterator)) {
1240+
if (strpbrk(entry->str, "*?[") &&
1241+
fnmatch(entry->str, path, FNM_PATHNAME | FNM_PERIOD) == 0)
1242+
return 1;
1243+
}
1244+
1245+
return 0;
1246+
}
1247+
11811248
/*
11821249
* Notes about elf program startup
11831250
* ===============================
@@ -1456,7 +1523,7 @@ static int check_subject(lnode *r, event_t *e)
14561523
!is_subj_trusted(e))
14571524
break;
14581525

1459-
if (!attr_set_check_str(r->s[cnt].set, subj->str))
1526+
if (!path_set_match(r->s[cnt].set, subj->str))
14601527
return 0;
14611528

14621529
break;
@@ -1592,14 +1659,6 @@ static decision_t check_object(lnode *r, event_t *e)
15921659

15931660
// fall through
15941661

1595-
case PATH:
1596-
// skip if fall through
1597-
if (type == PATH &&
1598-
legacy_untrusted_subject_path_blocked(r, cnt, e))
1599-
return 0;
1600-
1601-
// fall through
1602-
16031662
case DEVICE:
16041663
case FILE_HASH: {
16051664

@@ -1617,6 +1676,21 @@ static decision_t check_object(lnode *r, event_t *e)
16171676
} // case
16181677

16191678

1679+
case PATH: {
1680+
if (legacy_untrusted_subject_path_blocked(r, cnt, e))
1681+
return 0;
1682+
1683+
if (!obj->o)
1684+
break;
1685+
1686+
// Globbing changes rule selection only, never object identity.
1687+
if (!path_set_match(r->o[cnt].set, obj->o))
1688+
return 0;
1689+
1690+
break;
1691+
} // case
1692+
1693+
16201694
case ODIR: {
16211695
int macro_match = 0;
16221696

src/tests/attr_sets_test.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,18 @@ int main(void)
5454
error(1, 0, "standalone attr_set_create failed");
5555
if (attr_set_append_str(standalone, "/usr/bin/"))
5656
error(1, 0, "standalone append failed");
57+
if (standalone->has_glob)
58+
error(1, 0, "literal string marked as glob");
5759
if (!attr_set_check_str(standalone, "/usr/bin/"))
5860
error(1, 0, "standalone exact check failed");
5961
if (!attr_set_check_pstr(standalone, "/usr/bin/bash"))
6062
error(1, 0, "standalone prefix check failed");
6163
if (attr_set_append_str(standalone, "/usr/bin/") == 0)
6264
error(1, 0, "duplicate string accepted");
65+
if (attr_set_append_str(standalone, "/opt/app-*/bin/tool"))
66+
error(1, 0, "glob string append failed");
67+
if (!standalone->has_glob)
68+
error(1, 0, "glob string did not set lookup hint");
6369
attr_set_destroy(standalone);
6470

6571
attr_sets_destroy(sets);

0 commit comments

Comments
 (0)