@@ -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+
236314def 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