Skip to content

Commit 0097db1

Browse files
authored
Merge pull request #2065 from giuseppe/fix-apparmour-with-userns
utils: fix apparmor profile not applied in user namespaces
2 parents 8ae50fa + 94d7f64 commit 0097db1

File tree

2 files changed

+86
-19
lines changed

2 files changed

+86
-19
lines changed

src/libcrun/utils.c

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -750,34 +750,22 @@ int
750750
libcrun_initialize_apparmor (libcrun_error_t *err)
751751
{
752752
cleanup_close int fd = -1;
753-
int ret;
754753
int size;
755754
char buf[2];
756755

757756
if (apparmor_enabled >= 0)
758757
return apparmor_enabled;
759758

760-
ret = crun_dir_p_at (AT_FDCWD, "/sys/kernel/security/apparmor", true, err);
761-
if (UNLIKELY (ret < 0))
762-
{
763-
/* Directory doesn't exist — not an error, just no AppArmor. */
764-
crun_error_release (err);
765-
apparmor_enabled = 0;
766-
return 0;
767-
}
768-
769-
if (ret == 0)
770-
{
771-
/* Path exists but is not a directory — no AppArmor. */
772-
apparmor_enabled = 0;
773-
return 0;
774-
}
775-
776759
fd = open ("/sys/module/apparmor/parameters/enabled", O_RDONLY | O_CLOEXEC);
777760
if (fd == -1)
778761
{
779-
apparmor_enabled = 0;
780-
return 0;
762+
if (errno == ENOENT)
763+
{
764+
apparmor_enabled = 0;
765+
return 0;
766+
}
767+
768+
return crun_make_error (err, errno, "open `/sys/module/apparmor/parameters/enabled`");
781769
}
782770

783771
size = TEMP_FAILURE_RETRY (read (fd, &buf, 2));

tests/test_linux_features.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,84 @@ def test_process_apparmor_profile():
233233
return -1
234234

235235

236+
def test_process_apparmor_profile_userns():
237+
"""Test AppArmor profile is applied inside a user namespace.
238+
239+
Regression test: when a user namespace with UID/GID mappings is used,
240+
the AppArmor profile must still be applied. A refactoring of
241+
libcrun_initialize_apparmor() caused it to treat a failed stat on
242+
/sys/kernel/security/apparmor (which is expected inside a user
243+
namespace where securityfs is not accessible) as "AppArmor disabled",
244+
silently skipping the profile setup.
245+
"""
246+
247+
if not os.path.exists('/sys/kernel/security/apparmor'):
248+
return (77, "AppArmor not available")
249+
250+
if is_rootless():
251+
return (77, "requires root for user namespace with id mappings")
252+
253+
# Load a test AppArmor profile so we can distinguish "profile applied"
254+
# from "no profile" (both would show 'unconfined' otherwise).
255+
profile_name = "crun-test-userns"
256+
profile_text = """
257+
profile %s flags=(attach_disconnected) {
258+
/** rwlk,
259+
/** ix,
260+
capability,
261+
network,
262+
mount,
263+
umount,
264+
pivot_root,
265+
signal,
266+
ptrace,
267+
unix,
268+
}
269+
""" % profile_name
270+
271+
try:
272+
p = subprocess.run(["apparmor_parser", "--replace"],
273+
input=profile_text.encode(), capture_output=True)
274+
if p.returncode != 0:
275+
return (77, "cannot load test AppArmor profile")
276+
except FileNotFoundError:
277+
return (77, "apparmor_parser not found")
278+
279+
try:
280+
conf = base_config()
281+
add_all_namespaces(conf, userns=True)
282+
conf['process']['args'] = ['/init', 'cat', '/proc/1/attr/current']
283+
284+
conf['process']['apparmorProfile'] = profile_name
285+
286+
conf['linux']['uidMappings'] = [
287+
{'containerID': 0, 'hostID': os.getuid(), 'size': 65536}
288+
]
289+
conf['linux']['gidMappings'] = [
290+
{'containerID': 0, 'hostID': os.getgid(), 'size': 65536}
291+
]
292+
293+
out, _ = run_and_get_output(conf, hide_stderr=False)
294+
if profile_name not in out:
295+
logger.info("test failed: expected '%s' in output, got: %s",
296+
profile_name, out.strip())
297+
return -1
298+
return 0
299+
300+
except subprocess.CalledProcessError as e:
301+
output = e.output.decode('utf-8', errors='ignore') if e.output else ''
302+
if any(x in output.lower() for x in ["apparmor", "mount", "proc", "user", "mapping"]):
303+
return (77, "AppArmor with user namespace not available")
304+
logger.info("test failed: %s", e)
305+
return -1
306+
except Exception as e:
307+
logger.info("test failed: %s", e)
308+
return -1
309+
finally:
310+
subprocess.run(["apparmor_parser", "--remove"],
311+
input=profile_text.encode(), capture_output=True)
312+
313+
236314
def test_process_selinux_label():
237315
"""Test SELinux label."""
238316

@@ -1753,6 +1831,7 @@ def test_mqueue_mount():
17531831
"process-no-new-privileges": test_process_no_new_privileges,
17541832
"process-oom-score-adj": test_process_oom_score_adj,
17551833
"process-apparmor-profile": test_process_apparmor_profile,
1834+
"process-apparmor-profile-userns": test_process_apparmor_profile_userns,
17561835
"process-selinux-label": test_process_selinux_label,
17571836
"process-umask": test_process_umask,
17581837
"mount-label": test_mount_label,

0 commit comments

Comments
 (0)