From 0d1e60fae97c5a0859512d8d36ab216719811a85 Mon Sep 17 00:00:00 2001 From: Tomaz Zaman Date: Fri, 14 Aug 2026 00:41:58 +0200 Subject: [PATCH 60/72] layerscape: mono_gateway-dk: SELinux (dssp, permissive) + image tuning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrate a dssp/CIL SELinux policy, converged to zero AVC denials in permissive across boot, all stock daemons, the full LuCI UI (status/ realtime/firewall/nlbwmon/config), strongSwan/IPsec, SSH and htop — verified on hardware from the built image. - CONFIG_SELINUX + dssp; ext4 rootfs labeled at build time (image.mk) with a first-boot restorecon; held permissive via selinux-config. - Custom domains for the ASK/mono daemons (cmm, dpa_app, fancontrol, mono_update) and private device nodes (fman, cdx_ctrl, vwd). - stockfixes.cil: (in ) extensions closing every harvested denial for stock daemons + the LuCI web UI, incl. the status/ overview + Diagnostics collectors (devstatus/ipcalc/peeraddr/ vnstat/ping/traceroute) + netifd's connectivity-probe socket; transition-first for tools with their own domains (ip/tc/nft/uci/ubus/wg/firewall/ucode/kmod). Plus kernel- log denials the audit.log harvest missed — boarddetect at boot and fwtool on the sysupgrade tarball. - auditd: drain AVCs to /var/log/audit (the enforcing worklist). Image tuning alongside: - Add AdGuard Home (network DNS filtering). - Drop luci-app-package-manager: web apk install can't be made safe under enforcing on a sysupgrade-only appliance (CLI apk stays). - Drop sqm-scripts: CPU qdiscs fight the FMan/CDX hardware offload. Co-Authored-By: Claude Opus 4.8 --- configs/mono_gateway-dk.seed | 37 ++ include/image.mk | 15 + package/system/selinux-policy/Makefile | 7 + .../files/cil/cdxctrlnodedev.cil | 16 + .../selinux-policy/files/cil/cmmsysagent.cil | 106 ++++ .../files/cil/dpaappsysagent.cil | 71 +++ .../files/cil/fancontrolsysagent.cil | 58 +++ .../selinux-policy/files/cil/fmnodedev.cil | 49 ++ .../files/cil/monoupdatesysagent.cil | 66 +++ .../selinux-policy/files/cil/stockfixes.cil | 463 ++++++++++++++++++ .../selinux-policy/files/cil/vwdnodedev.cil | 18 + .../selinux-policy/files/selinux-config | 4 +- .../etc/uci-defaults/99-mono-selinux-relabel | 18 + target/linux/layerscape/image/armv8_64b.mk | 10 +- .../layerscape/image/gen_ext4_resizable.sh | 116 +++++ 15 files changed, 1050 insertions(+), 4 deletions(-) create mode 100644 package/system/selinux-policy/files/cil/cdxctrlnodedev.cil create mode 100644 package/system/selinux-policy/files/cil/cmmsysagent.cil create mode 100644 package/system/selinux-policy/files/cil/dpaappsysagent.cil create mode 100644 package/system/selinux-policy/files/cil/fancontrolsysagent.cil create mode 100644 package/system/selinux-policy/files/cil/fmnodedev.cil create mode 100644 package/system/selinux-policy/files/cil/monoupdatesysagent.cil create mode 100644 package/system/selinux-policy/files/cil/stockfixes.cil create mode 100644 package/system/selinux-policy/files/cil/vwdnodedev.cil create mode 100644 target/linux/layerscape/base-files/etc/uci-defaults/99-mono-selinux-relabel create mode 100755 target/linux/layerscape/image/gen_ext4_resizable.sh diff --git a/configs/mono_gateway-dk.seed b/configs/mono_gateway-dk.seed index d8c87bb1b4..c72858337d 100644 --- a/configs/mono_gateway-dk.seed +++ b/configs/mono_gateway-dk.seed @@ -52,3 +52,40 @@ CONFIG_PACKAGE_u-boot-fsl_ls1046a-rdb-sdboot=y # CONFIG_TARGET_IMAGES_GZIP is not set CONFIG_TARGET_ROOTFS_PARTSIZE=256 # CONFIG_TARGET_ROOTFS_SQUASHFS is not set +# SELinux — permissive skeleton. CONFIG_SELINUX=y cascades the +# kernel LSM bits, swaps in procd-selinux/busybox-selinux, and pulls the dssp +# base policy. The rootfs is held permissive via `enforcing=0` on the kernel +# cmdline (Build/mono-bootfs) until per-daemon policy is authored; first boot +# labels the ext4 rootfs via restorecon (uci-defaults 15-mono-selinux-relabel). +CONFIG_SELINUX=y +CONFIG_SELINUXTYPE_dssp=y +# On-device relabel tooling: restorecon/setfiles (first-boot ext4 relabel) + sestatus +# (verification). policycoreutils is a MENU package, so DEVICE_PACKAGES alone leaves it +# unbuilt — it needs explicit selection here. Pulls libaudit. +CONFIG_PACKAGE_policycoreutils=y +CONFIG_PACKAGE_policycoreutils-setfiles=y +CONFIG_PACKAGE_policycoreutils-sestatus=y +# auditd drains SELinux AVC records to /var/log/audit/audit.log. Once it claims +# the audit netlink the kernel stops printk'ing denials to the console, keeping +# the serial console clean for kernel diag prints; ausearch (audit-utils) reads +# the log. No audisp-syslog mirror, so denials do NOT also go to logread. +# Pulls libauparse + audit-utils + libev. +CONFIG_PACKAGE_auditd=y + +# AdGuard Home — network-wide DNS ad/tracker blocker with its own web UI (:3000). +# From the pinned packages feed (not a bulk FEED_packages enable — just this pkg). +# Go SOURCE build: pulls the golang/host toolchain (heavy first build) + ca-bundle; +# ships /etc/config/adguardhome + adguardhome.yaml conffiles + init + a procd +# capabilities profile. Will need its own SELinux domain later (author from soak). +CONFIG_PACKAGE_adguardhome=y + +# Drop LuCI's Software (apk) WEB frontend only. It made the cgi-io helper exec apk, +# dragging a large SELinux AVC surface (apk data/conf/cache reads + exec) into the +# web path, and web-driven package management isn't our delivery model (sysupgrade +# is). This removes ONLY the LuCI page: the apk binary stays (base system) and must +# remain fully usable from the shell — nothing here disables CLI apk. luci-ssl was +# swapped for luci-light + libustream-mbedtls + px5g-mbedtls in the device +# DEVICE_PACKAGES (that collection was the only thing pulling package-manager); this +# line stops the stale =y from re-installing it. (Keeping shell apk AVC-clean under +# enforcing is future policy work on the login/admin domain, not precluded here.) +# CONFIG_PACKAGE_luci-app-package-manager is not set diff --git a/include/image.mk b/include/image.mk index 7888dfba42..004a3c51a7 100644 --- a/include/image.mk +++ b/include/image.mk @@ -314,6 +314,20 @@ define Image/mkfs/ubifs -o $@ -d $(call mkfs_target_dir,$(1)) endef +# Mono: when SELinux labels are requested, build the ext4 rootfs with setfiles + +# mkfs.ext4 -d in one fakeroot session so the security.selinux xattrs are baked into +# the image at build time (make_ext4fs does not copy source xattrs). Ported from the +# cvd fork (gen_ext4_resizable.sh); removes the first-boot relabel+reboot. See SELINUX.md. +ifeq ($(CONFIG_TARGET_ROOTFS_SECURITY_LABELS),y) +define Image/mkfs/ext4 + $(TOPDIR)/target/linux/layerscape/image/gen_ext4_resizable.sh \ + $@ $(ROOTFS_PARTSIZE) $(CONFIG_TARGET_EXT4_BLOCKSIZE) \ + $(call mkfs_target_dir,$(1)) \ + $(if $(CONFIG_TARGET_EXT4_RESERVED_PCT),$(CONFIG_TARGET_EXT4_RESERVED_PCT),0) \ + $(if $(CONFIG_TARGET_EXT4_JOURNAL),journal,nojournal) \ + $(STAGING_DIR_HOST)/bin/setfiles +endef +else define Image/mkfs/ext4 $(STAGING_DIR_HOST)/bin/make_ext4fs -L rootfs \ -l $(ROOTFS_PARTSIZE) -b $(CONFIG_TARGET_EXT4_BLOCKSIZE) \ @@ -322,6 +336,7 @@ define Image/mkfs/ext4 $(if $(SOURCE_DATE_EPOCH),-T $(SOURCE_DATE_EPOCH)) \ $@ $(call mkfs_target_dir,$(1))/ endef +endif # Don't use the mkfs.erofs builtin $SOURCE_DATE_EPOCH behavior define Image/mkfs/erofs diff --git a/package/system/selinux-policy/Makefile b/package/system/selinux-policy/Makefile index 2d329e2c7f..6636b5aa9b 100644 --- a/package/system/selinux-policy/Makefile +++ b/package/system/selinux-policy/Makefile @@ -33,6 +33,13 @@ define Package/selinux-policy/description OpenWrt and written in Common Intermediate Language. endef +define Build/Prepare + $(call Build/Prepare/Default) + $(INSTALL_DIR) $(PKG_BUILD_DIR)/src/agent/sysagent $(PKG_BUILD_DIR)/src/dev/nodedev + $(CP) ./files/cil/cmmsysagent.cil ./files/cil/dpaappsysagent.cil ./files/cil/fancontrolsysagent.cil ./files/cil/monoupdatesysagent.cil ./files/cil/stockfixes.cil $(PKG_BUILD_DIR)/src/agent/sysagent/ + $(CP) ./files/cil/cdxctrlnodedev.cil ./files/cil/fmnodedev.cil ./files/cil/vwdnodedev.cil $(PKG_BUILD_DIR)/src/dev/nodedev/ +endef + define Build/Compile $(call Build/Compile/Default,policy) endef diff --git a/package/system/selinux-policy/files/cil/cdxctrlnodedev.cil b/package/system/selinux-policy/files/cil/cdxctrlnodedev.cil new file mode 100644 index 0000000000..f4e2524945 --- /dev/null +++ b/package/system/selinux-policy/files/cil/cdxctrlnodedev.cil @@ -0,0 +1,16 @@ +;; cdxctrlnodedev.cil — private label for /dev/cdx_ctrl +;; char dev created by cdx.ko; devtmpfs auto-creates the node. Granted ONLY to cmm_t + dpaapp_t. +;; Mirrors dev/nodedev/pppnodedev.cil verbatim. +(block cdxctrl + + (filecon "/dev/cdx_ctrl" char nodedev_context) + + (macro tmp_fs_type_transition_nodedev ((type ARG1)) + (call .tmp.fs_type_transition + (ARG1 nodedev chr_file "cdx_ctrl"))) + + (blockinherit .nodedev.template)) + +(in nodedev.unconfined + + (call .cdxctrl.tmp_fs_type_transition_nodedev (typeattr))) diff --git a/package/system/selinux-policy/files/cil/cmmsysagent.cil b/package/system/selinux-policy/files/cil/cmmsysagent.cil new file mode 100644 index 0000000000..774cdc3f54 --- /dev/null +++ b/package/system/selinux-policy/files/cil/cmmsysagent.cil @@ -0,0 +1,106 @@ +;; cmmsysagent.cil — cmm_t (PRIMARY) +;; procd -> /usr/sbin/cmm-supervise (shell, stays sys.subj) -> exec /usr/bin/cmm (THIS transition). +;; `cmm -c` CLI is the SAME exec => re-enters cmm_t, so SysV-msgq IPC + loopback control self-scope. +(block cmm + + (blockinherit .sys.agent.template) + + ;; net_admin: rtnl + cdx_ctrl ioctl + NETLINK_FF/L2FLOW; net_raw: AF_PACKET/pcap + (allow subj self (capability (net_admin net_raw))) + ;; sys_module: kernel-module autoload on the offload path (pairs with .sys.modulerequest_system + ;; below); ipc_owner: own the SysV msgq that `cmm -c` uses to reach the daemon. + (allow subj self (capability (sys_module ipc_owner))) + (call .sys.modulerequest_system (subj)) ;; trigger kernel-module autoload (see sys_module) + (allow subj self create_netlink_route_socket) + (allow subj self (netlink_route_socket (nlmsg_read nlmsg_write))) + (allow subj self create_netlink_netfilter_socket) ;; libnetfilter_conntrack (NETLINK_NETFILTER) + (allow subj self create_netlink_socket) ;; NETLINK_FF/L2FLOW => generic class; cmm_t ONLY + (allow subj self create_packet_socket) ;; AF_PACKET + libpcap + (allow subj self create_rawip_socket) ;; SOCK_RAW ICMPv6/IP + (allow subj self create_tcp_stream_socket) ;; loopback CLI listener + (allow subj self create_udp_socket) ;; datagram socket (cmm control/probe) + (allow subj self create_msgq) ;; `cmm -c` CLI <-> daemon + (allow subj self (msg (send receive))) + (allow subj self (process (getpgid))) ;; query own process-group id + + (call .net.nodebind_netnode_tcp_sockets (subj)) + (call .net.egress_netifs (subj)) + (call .net.sendto_nodes (subj)) + (call .net.port.unreserved.namebind_all_tcp_sockets (subj)) ;; loopback CLI listener binds an ephemeral tcp port + + ;; tune /proc/sys/net/netfilter/nf_conntrack_tcp_be_liberal (net.sysctlfile) for the offload + (call .sysctl.search_procfile_dirs (subj)) ;; traverse /proc/sys + (call .net.search_sysctlfile_dirs (subj)) ;; traverse /proc/sys/net[/netfilter] + (call .net.write_sysctlfile_files (subj)) ;; write the conntrack sysctl (write_file incl. open) + + ;; flush the IPv4 route cache (/proc/sys/net/ipv4/route/flush) on network restart. ipv4.sysctlfile + ;; is a DISTINCT type from the net.sysctlfile written above. + (call .ipv4.search_sysctlfile_dirs (subj)) ;; traverse /proc/sys/net/ipv4[/route] + (call .ipv4.write_sysctlfile_files (subj)) ;; write route/flush (write_file incl. open) + + ;; read /proc/net/* (route/conntrack tables). /proc/net is a symlink => lnk_file read too. + (call .net.read_procfile_files (subj)) ;; (file (open read)) + (call .net.read_procfile_lnk_files (subj)) ;; (lnk_file (read)) — the /proc/net symlink + + (call .cdxctrl.readwrite_nodedev_chr_files (subj)) ;; /dev/cdx_ctrl + (call .vwd.read_nodedev_chr_files (subj)) ;; /dev/vwd0 virtual watchdog — read/open/ioctl, NO write + (call .console.open_serialtermdev_chr_files (subj)) ;; open /dev/console (diagnostics) + + (call .log.server.unix_dgram_send (subj)) ;; syslog + + ;; /etc/config/fastforward is a UCI config file labeled uci.conf.file (by ucisysagent) so `uci` + ;; (uci.subj) manages it; cmm only READS it. That is a DISTINCT type from the generic conf.file, + ;; so it needs the uci-specific read/search interface (not .conf.read_file_files). + (call .file.conf.uci.search_all_dirs (subj)) ;; (dir (search)) — traverse /etc/config + (call .file.conf.uci.read_all_files (subj)) ;; (file (open read)) — /etc/config/* + + ;; other generic /etc config files cmm reads (non-UCI). + (call .conf.read_file_files (subj)) + + ;; SysV-IPC ftok key file /tmp/cmm. -> private cmm.tmp type. The subj-sourced "*" transition + ;; (dnsmasq idiom) labels whatever cmm creates in /tmp and grants add_name+search on /tmp (tmp.fs); + ;; deletename_fs_dirs adds remove_name for cleanup. Closes the round-1 .tmp.search_fs_dirs TODO. + (call tmp.manage_file_files (subj)) + (call tmp.tmp_fs_type_transition_file (subj file "*")) + (call .tmp.deletename_fs_dirs (subj)) + + ;; pidfile /var/run/cmm.pid (=> /tmp/run/cmm.pid) -> private runtmp type; name-transition grants + ;; add_name+write on /tmp/run, deletename_file_dirs adds remove_name for shutdown cleanup. + (call runtmp.manage_file_files (subj)) + (call runtmp.runtmp_file_type_transition_file (subj)) + (call .runtmp.deletename_file_dirs (subj)) + + (block tmp + + (filecon "/tmp/cmm\..*" file file_context) + + (macro tmp_fs_type_transition_file ((type ARG1)(class ARG2)(name ARG3)) + (call .tmp.fs_type_transition + (ARG1 file ARG2 ARG3))) + + (blockinherit .file.macro_template_files) + (blockinherit .file.tmp.base_template)) + + (block runtmp + + (filecon "/tmp/run/cmm\.pid" file file_context) + + (macro runtmp_file_type_transition_file ((type ARG1)) + (call .runtmp.file_type_transition + (ARG1 file file "cmm.pid"))) + + (blockinherit .file.macro_template_files) + (blockinherit .file.tmp.run.base_template))) + +(in file.unconfined + + (call .cmm.runtmp.runtmp_file_type_transition_file (typeattr)) + (call .cmm.exec.exec_file_type_transition_file (typeattr))) + +(in after cmm.exec + + (filecon "/usr/bin/cmm" file file_context) + + (macro exec_file_type_transition_file ((type ARG1)) + (call .exec.file_type_transition + (ARG1 file file "cmm")))) diff --git a/package/system/selinux-policy/files/cil/dpaappsysagent.cil b/package/system/selinux-policy/files/cil/dpaappsysagent.cil new file mode 100644 index 0000000000..a5abbf1bbd --- /dev/null +++ b/package/system/selinux-policy/files/cil/dpaappsysagent.cil @@ -0,0 +1,71 @@ +;; dpaappsysagent.cil — dpa_app_t +;; KERNEL-SPAWNED one-shot: cdx.ko call_usermodehelper("/usr/bin/dpa_app", UMH_WAIT_PROC). +;; UMH runs under kernel SID => sys.subj => .sys.agent.template transition fires with no bespoke rule. +(block dpaapp + + (blockinherit .sys.agent.template) + + (allow subj self (capability (net_admin))) ;; cdx_ctrl / fm ioctl gate + + (call .cdxctrl.readwrite_nodedev_chr_files (subj)) ;; /dev/cdx_ctrl + (call .fm.readwrite_nodedev_chr_files (subj)) ;; /dev/fm* (FMan) — program the HW offload + + (call conf.read_file_files (subj)) ;; /etc/cdx_*.xml (private dpaapp.conf type) + + ;; FMC PCD config XMLs under /etc/fmc/config/ (hxs_pdl_v3.xml et al.) ship with the generic /etc + ;; (conf.file) label. The subj template already grants .conf.search_file_dirs (subj.cil:123), so + ;; add generic conf file-read + dir-list (dpa_app is a trusted boot-time programmer, read-only /etc). + (call .conf.list_file_dirs (subj)) + (call .conf.read_file_files (subj)) + + ;; dpa_app writes a generated header to /softparse.h directly under / . + ;; QUIRK: review whether dpa_app should write this somewhere other than the root dir. + (call data.root_file_type_transition_file (subj)) + (call data.manage_file_files (subj)) + + (call .log.server.unix_dgram_send (subj)) + + (block conf + + ;; NOTE: enumerate any further cdx_*.xml as permissive AVCs reveal them. + (filecon "/etc/cdx_pcd\.xml" file file_context) + (filecon "/etc/cdx_sp\.xml" file file_context) + (filecon "/etc/cdx_cfg\.xml" file file_context) + + (macro conf_file_type_transition_file ((type ARG1)) + (call .conf.file_type_transition + (ARG1 file file "cdx_pcd.xml")) + (call .conf.file_type_transition + (ARG1 file file "cdx_sp.xml")) + (call .conf.file_type_transition + (ARG1 file file "cdx_cfg.xml"))) + + (blockinherit .file.conf.template)) + + (block data + + ;; /softparse.h — generated by dpa_app in the root dir + (filecon "/softparse\.h" file file_context) + + (macro root_file_type_transition_file ((type ARG1)) + (call .root.file_type_transition + (ARG1 file file "softparse.h"))) + + (blockinherit .file.base_template) + (blockinherit .file.macro_template_files) + + (call .xattr.client.associate_fs.type (file)))) + +(in file.unconfined + + (call .dpaapp.conf.conf_file_type_transition_file (typeattr)) + (call .dpaapp.data.root_file_type_transition_file (typeattr)) + (call .dpaapp.exec.exec_file_type_transition_file (typeattr))) + +(in after dpaapp.exec + + (filecon "/usr/bin/dpa_app" file file_context) + + (macro exec_file_type_transition_file ((type ARG1)) + (call .exec.file_type_transition + (ARG1 file file "dpa_app")))) diff --git a/package/system/selinux-policy/files/cil/fancontrolsysagent.cil b/package/system/selinux-policy/files/cil/fancontrolsysagent.cil new file mode 100644 index 0000000000..e80c617f14 --- /dev/null +++ b/package/system/selinux-policy/files/cil/fancontrolsysagent.cil @@ -0,0 +1,58 @@ +;; fancontrolsysagent.cil — fancontrol_t +;; procd execs the fancontrol bash script (physically /usr/sbin/fancontrol; the filecon uses the +;; /usr/bin canonical path — file_contexts.subs_dist maps /usr/sbin -> /usr/bin). Sysfs mirrors ledsysagent. +(block fancontrol + + (blockinherit .sys.agent.template) + + ;; /sys/class/hwmon discovery, read fan inputs, WRITE pwmN (ledsysagent pattern) + (call .class.list_sysfile_dirs (subj)) + (call .class.read_sysfile_lnk_files (subj)) + (call .devices.list_sysfile_dirs (subj)) + (call .devices.read_sysfile_files (subj)) + (call .devices.read_sysfile_lnk_files (subj)) + (call .devices.readwrite_sysfile_files (subj)) ;; pwmN write + + (call .shell.client.type (subj)) ;; busybox/coreutils helpers + + ;; fancontrol is a #!/bin/bash script; /bin/bash is the generic exec.file type (not shell.exec), so + ;; the domain needs execute+map+read on generic executables to run the interpreter (ttydsysagent + ;; pattern). map is required on modern kernels for the mmap of the executable image. + (call .file.exec.mapexecute_all_files (subj)) ;; execute + map + (call .file.exec.read_all_files (subj)) ;; read + + ;; pidfile /var/run/fancontrol.pid (=> /tmp/run/fancontrol.pid) — private runtmp type; the + ;; name-transition grants subj add_name+write on the /tmp/run (runtmp.file) dir, and + ;; deletename_file_dirs adds remove_name so the init stop() `rm` can delete the pidfile. + (call runtmp.manage_file_files (subj)) + (call runtmp.runtmp_file_type_transition_file (subj)) + (call .runtmp.deletename_file_dirs (subj)) + + (call .log.server.unix_dgram_send (subj)) + + (block runtmp + + (filecon "/tmp/run/fancontrol" file file_context) + (filecon "/tmp/run/fancontrol\.pid" file file_context) + + (macro runtmp_file_type_transition_file ((type ARG1)) + (call .runtmp.file_type_transition + (ARG1 file file "fancontrol")) + (call .runtmp.file_type_transition + (ARG1 file file "fancontrol.pid"))) + + (blockinherit .file.macro_template_files) + (blockinherit .file.tmp.run.base_template))) + +(in file.unconfined + + (call .fancontrol.exec.exec_file_type_transition_file (typeattr)) + (call .fancontrol.runtmp.runtmp_file_type_transition_file (typeattr))) + +(in after fancontrol.exec + + (filecon "/usr/bin/fancontrol" file file_context) ;; canonical path (binary is /usr/sbin/fancontrol) + + (macro exec_file_type_transition_file ((type ARG1)) + (call .exec.file_type_transition + (ARG1 file file "fancontrol")))) diff --git a/package/system/selinux-policy/files/cil/fmnodedev.cil b/package/system/selinux-policy/files/cil/fmnodedev.cil new file mode 100644 index 0000000000..1a4562a520 --- /dev/null +++ b/package/system/selinux-policy/files/cil/fmnodedev.cil @@ -0,0 +1,49 @@ +;; fmnodedev.cil — private label for the Freescale FMan char devices (/dev/fm*) +;; fsl_fman (DEV_FM_NAME="fm") creates, per FMan: /dev/fm0, /dev/fm0-pcd, and the port nodes +;; /dev/fm0-port-{rx,tx}0..7 + /dev/fm0-port-oh0..6. +;; [include/uapi/linux/fmd/Peripherals/fm_ioctls.h:53-59; sdk_fman/.../lnxwrp_fm.c:1165-1168; +;; lnxwrp_fm_port.c:1163-1203; LS1043 integration dpaa_integration_ext.h:192-203 => +;; FM_MAX_NUM_OF_MACS=8 (6x1G+2x10G) => rx/tx 0..7; FM_MAX_NUM_OF_OH_PORTS=6 => oh0..6] +;; dpa_app opens all of these to program the FMan PCD for CDX hardware offload. +;; Device-family node (mirrors dev/nodedev/usbnodedev.cil): enumerated devtmpfs name-transitions +;; rather than a blanket tmp.fs chr_file grant to dpaapp — keeps every fm node in fm.nodedev, so the +;; grant stays tight and no domain gets access to arbitrary unlabeled devtmpfs char nodes. LS1046A +;; has a single FMan (fm0). Extra names for ports the board never probes are harmless no-ops. +(block fm + + (filecon "/dev/fm[0-9]+" char nodedev_context) + (filecon "/dev/fm[0-9]+-pcd" char nodedev_context) + (filecon "/dev/fm[0-9]+-port-(oh|rx|tx)[0-9]+" char nodedev_context) + + (macro tmp_fs_type_transition_nodedev ((type ARG1)) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-pcd")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-rx0")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-rx1")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-rx2")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-rx3")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-rx4")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-rx5")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-rx6")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-rx7")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-tx0")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-tx1")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-tx2")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-tx3")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-tx4")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-tx5")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-tx6")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-tx7")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-oh0")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-oh1")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-oh2")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-oh3")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-oh4")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-oh5")) + (call .tmp.fs_type_transition (ARG1 nodedev chr_file "fm0-port-oh6"))) + + (blockinherit .nodedev.template)) + +(in nodedev.unconfined + + (call .fm.tmp_fs_type_transition_nodedev (typeattr))) diff --git a/package/system/selinux-policy/files/cil/monoupdatesysagent.cil b/package/system/selinux-policy/files/cil/monoupdatesysagent.cil new file mode 100644 index 0000000000..3570ce18c1 --- /dev/null +++ b/package/system/selinux-policy/files/cil/monoupdatesysagent.cil @@ -0,0 +1,66 @@ +;; monoupdatesysagent.cil — mono_update_t (HIGH-CONSEQUENCE: can trigger a flash) +;; cron-spawned mono-update-check (/bin/sh: curl -> usign -> sysupgrade). busybox crond has NO dssp +;; domain => runs sys.subj => transition fires by the same template. The binary physically lives at +;; /usr/sbin/mono-update-check, but the filecon MUST use the /usr/bin canonical path: +;; file_contexts.subs_dist maps /usr/sbin -> /usr/bin, so a /usr/sbin/* filecon is never consulted +;; (the lookup hits the /usr/bin/.* exec catch-all and the domain never transitions). +(block monoupdate + + (blockinherit .sys.agent.template) + + (allow subj self create_tcp_stream_socket) + + (call .shell.client.type (subj)) ;; exec busybox/coreutils + (call .dns.client.type (subj)) + + (call .net.nodebind_netnode_tcp_sockets (subj)) + (call .http.reserved.nameconnect_port_tcp_sockets (subj)) ;; port 80 (redirect) + (call .http.secure.reserved.nameconnect_port_tcp_sockets (subj)) ;; port 443 (HTTPS OTA) + + ;; ca-bundle TLS trust store (acmesysagent pattern) + (call .cert.list_file_dirs (subj)) + (call .cert.read_file_files (subj)) + (call .cert.read_file_lnk_files (subj)) + + ;; run /usr/bin/usign in mono_update_t to verify the release signature + ;; (acme's helper-exec idiom; usign exposes no 1-arg subj_type_transition) + (call .usign.exec.execute_file_files (subj)) + + ;; TODO(avc): transition into the sysupgrade domain when a flash is triggered. + ;; Real macro is .sysupgrade.exec.subj_type_transition (2-arg; + ;; file/datafile/dataexecfile/sysupgradedataexecfile.cil:22-27). Pin from permissive AVCs. + ;; NOTE: .net.egress_netifs / .net.sendto_nodes intentionally left for AVC discovery on the HTTPS path. + + (call .log.server.unix_dgram_send (subj)) + + (call conf.list_file_dirs (subj)) ;; /etc/mono-keys/ (private daemon-owned) + (call conf.read_file_files (subj)) ;; /etc/mono_release + keys (private daemon-owned) + + ;; /etc/config/mono-update is a UCI config: it MUST stay the generic conf.file type so `uci` + ;; (uci.subj) can manage it. monoupdate only READS it -> generic conf read (no private label). + (call .conf.read_file_files (subj)) + + (block conf + + ;; daemon-owned (NOT UCI): keep private so only mono_update_t reads them + (filecon "/etc/mono_release" file file_context) + (filecon "/etc/mono-keys(/.*)?" any file_context) + + (macro conf_file_type_transition_file ((type ARG1)) + (call .conf.file_type_transition + (ARG1 file file "mono_release"))) + + (blockinherit .file.conf.template))) + +(in file.unconfined + + (call .monoupdate.conf.conf_file_type_transition_file (typeattr)) + (call .monoupdate.exec.exec_file_type_transition_file (typeattr))) + +(in after monoupdate.exec + + (filecon "/usr/bin/mono-update-check" file file_context) ;; canonical path (binary is /usr/sbin/mono-update-check) + + (macro exec_file_type_transition_file ((type ARG1)) + (call .exec.file_type_transition + (ARG1 file file "mono-update-check")))) diff --git a/package/system/selinux-policy/files/cil/stockfixes.cil b/package/system/selinux-policy/files/cil/stockfixes.cil new file mode 100644 index 0000000000..78cd652e3a --- /dev/null +++ b/package/system/selinux-policy/files/cil/stockfixes.cil @@ -0,0 +1,463 @@ +;; SPDX-FileCopyrightText: © 2026 Mono OpenWrt +;; SPDX-License-Identifier: Unlicense +;; +;; stockfixes.cil — close permissive-mode AVC denials against STOCK dssp +;; daemons on the Mono Gateway. Every rule here EXTENDS an existing stock +;; domain via (in ...); no dssp base .cil file is patched. Harvest: +;; /var/log/audit/audit.log (permissive) on hardware. +;; +;; Transition-first rule: when a domain execs a tool that already has its own +;; dssp domain, add the tool's subj_type_transition so the tool runs confined +;; in its own domain instead of raw execute/execute_no_trans in the caller. +;; The agent-template one-arg subj_type_transition macro grants the caller +;; read_file (getattr ioctl lock open read) + mapexecute_file (execute map) on +;; the target's exec.file plus the process typetransition — so it also closes +;; the pre-exec getattr/stat on that exec file, and does NOT need +;; execute_no_trans (the transition path is taken instead). + +;; =========================================================================== +;; odhcp.server (odhcpd) — private-label the atomic-write temp paths +;; =========================================================================== +;; Denials: file tmp.fs {create open write rename lock ioctl unlink}; +;; dir tmp.fs {create}. +;; odhcpd stages the lease file through /tmp/.odhcpd.tmp (write-then-rename) +;; and uses a /tmp/odhcpd-piodir working dir. With no name-transition these +;; land as the shared tmp.fs type, which odhcpd may not manage. Give them the +;; private odhcp.server.tmp.file label — already covered by the domain's +;; tmp.manage_file_{files,dirs} — via the same filecons + fs_type_transitions +;; the base file already uses for .odhcpd.leases / odhcpd-piofolder. Adapted +;; from cvd's bring-up patch (010), re-expressed as (in ...); base untouched. + +(in odhcp.server + (call tmp.tmp_fs_type_transition_file (subj dir "odhcpd-piodir")) + (call tmp.tmp_fs_type_transition_file (subj file ".odhcpd.tmp"))) + +(in odhcp.server.tmp + (filecon "/tmp/\.odhcpd\.tmp" file file_context) + (filecon "/tmp/\.odhcpd\.tmp\..*" file file_context) + (filecon "/tmp/odhcpd-piodir" dir file_context) + (filecon "/tmp/odhcpd-piodir/.*" any file_context)) + +(in file.unconfined + (call .odhcp.server.tmp.tmp_fs_type_transition_file + (typeattr dir "odhcpd-piodir")) + (call .odhcp.server.tmp.tmp_fs_type_transition_file + (typeattr file ".odhcpd.tmp"))) + +;; =========================================================================== +;; ucode — LuCI backend runs as ucode +;; =========================================================================== +;; Denials: file luci.data.file {read open}; dir luci.data.file {search read +;; open}; file exec.file {read open}; file conftmp.file {write ioctl}. +;; ucode hosts the LuCI ucode backend: it reads /usr/share/luci (luci.data), +;; reads a generic executable's bytes, and stages config into /tmp/etc +;; (conftmp) on save-and-apply. + +(in ucode + (call .luci.data.list_file_dirs (subj)) + (call .luci.data.read_file_files (subj)) + + ;; read (not execute) the bytes of a generic-labelled executable + (call .exec.read_file_files (subj)) + + ;; write staged config under /tmp/etc + (call .conftmp.write_file_files (subj))) + +;; =========================================================================== +;; hotplug.call — run routing/netfilter tools confined; native probes/state +;; =========================================================================== +;; hotplug handler scripts exec ip/tc/nft. Confine them in their own domains. +;; This ALSO absorbs the harvested self netlink_route_socket, net_admin +;; capability, /proc/net reads and /dev/urandom reads: those were ip/tc/nft +;; running in hotplug.call.subj (no transition), and iproute2.ip / iproute2.tc +;; / nft already carry exactly those perms — so they are deliberately NOT +;; re-granted here (see the "prefer transitions" rule and the handoff report). + +(in hotplug.call + (call .iproute2.ip.subj_type_transition (subj)) + (call .iproute2.tc.subj_type_transition (subj)) + ;; read_file from the transition covers the harvested nft.exec.file getattr + (call .nft.subj_type_transition (subj)) + + ;; getattr-only executability probes ([ -x ... ]) on tools NOT exec'd here + ;; (no execute was harvested). read_file is the narrowest catalogued + ;; interface — there is no getattr-only file interface in dssp. + (call .kmod.loader.exec.read_file_files (subj)) + (call .exec.read_file_files (subj)) + + ;; genuinely hotplug-native accesses (not attributable to a confined tool): + (allow subj self (process (getsched))) + (call .cpuinfo.read_procfile_files (subj)) ;; scripts count CPUs + (call .sys.modulerequest_system (subj)) ;; device-module autoload + (call .conf.read_file_files (subj)) ;; generic /etc config reads + (call .conf.list_file_dirs (subj)) ;; +read/open conf.file dirs (surfaced by openvpn-up hotplug) + (call .runtmp.manage_file_dirs (subj)) ;; manage generic /tmp/run + (call .runtmp.manage_file_files (subj))) + +;; =========================================================================== +;; rpc.server (rpcd) — shell plugins: package DB, shell libs, jshn, generic exec +;; =========================================================================== +;; Denials: {read open search} on apk.data + libubox.data; {read open getattr +;; search} on functions.data; jshn.exec {read open map execute_no_trans}; +;; exec.file (file+dir) {read open getattr execute execute_no_trans}. + +(in rpc.server + ;; the apk package status DB, libubox + shell function libraries + (call .apk.data.list_file_dirs (subj)) + (call .apk.data.read_file_files (subj)) + (call .libubox.data.list_file_dirs (subj)) + (call .libubox.data.read_file_files (subj)) + ;; functions.client = functions.data list+read (covers search + getattr) + (call .functions.client.type (subj)) + + ;; plugins exec the jshn helper — confine it in its own domain + (call .jshn.subj_type_transition (subj)) + + ;; plugins exec generic shell scripts (/usr/libexec/rpcd/*) in-domain: + ;; list the dir, execute_file grants the execute + execute_no_trans set + (call .exec.list_file_dirs (subj)) + (call .exec.execute_file_files (subj))) + +;; =========================================================================== +;; jshn — generic /tmp/run I/O and /etc config reads +;; =========================================================================== +;; Denials: file runtmp.file {write read open ioctl getattr}; +;; file conf.file {read open getattr}. + +(in jshn + (call .runtmp.readwrite_file_files (subj)) + (call .conf.read_file_files (subj))) + +;; =========================================================================== +;; ipcalc — silence the inherited netif.data fd +;; =========================================================================== +;; Denial: file netif.data.file {read} — read with no open => an already-open +;; fd leaked from netifd (the parent), not a file ipcalc opens itself. Mirror +;; how ucode / jshn / nft / odhcpd handle the same leak: dontaudit, not grant. + +(in ipcalc + (call .netif.data.dontaudit_readinherited_file_files (subj))) + +;; =========================================================================== +;; iproute2.tc — silence the inherited netifd fd/data leak +;; =========================================================================== +;; Denials appeared only AFTER tc was confined via the hotplug ip/tc transition: +;; file netif.data.file {read} + fd netif.server.subj {use} — both leaked from +;; netifd (tc's parent), not opened by tc. dontaudit, mirroring ipcalc. + +(in iproute2.tc + (call .netif.data.dontaudit_readinherited_file_files (subj)) + (call .netif.server.dontaudit_use_subj_fds (subj))) + +;; =========================================================================== +;; hotplug.call — /dev/urandom read (hotplug-native, e.g. update-check script) +;; =========================================================================== +;; Denial: chr_file random.nodedev {read open} by a script in hotplug.call.subj +;; (comm=update-availabl). Persisted after the ip/tc/nft transitions, so it is +;; genuinely hotplug-native (not an in-domain tool) — grant it. + +(in hotplug.call + (call .random.read_nodedev_chr_files (subj))) + +;; =========================================================================== +;; dnsmasq.server — bind DNS on an unreserved port (:5353) +;; =========================================================================== +;; Denials: udp_socket + tcp_socket name_bind on unreserved.port. dnsmasq was +;; moved off :53 to :5353 (AdGuard Home owns :53 as the filtering resolver), so +;; it now binds an unreserved port; the base policy only granted the reserved +;; dns.port. Grant the unreserved bind on both socket types. + +(in dnsmasq.server + (call .net.port.unreserved.namebind_all_tcp_sockets (subj)) + (call .net.port.unreserved.namebind_all_udp_sockets (subj))) + +;; =========================================================================== +;; luci — service/status + process/overview pages +;; =========================================================================== +;; Two access profiles, both driven by browsing the LuCI admin UI: +;; +;; (1) Service/Startup pages stat init scripts and read their configs: +;; getattr on initscript.file (+ dir search) and conf.file. No getattr-only +;; file interface exists in dssp (getattr ⊂ read_file's getattr/ioctl/lock/ +;; open/read set), so read_file is the narrowest catalogued grant; these are +;; the same interfaces initscriptsysagent/hotplug/rpcd/jshn already use. +;; +;; (2) Status→Overview / realtime / System→Processes enumerate EVERY running +;; process: read each daemon's /proc/ (dir+file labelled with that +;; daemon's own .subj type), signal-0 liveness probes, and list +;; /proc itself. htop (the stock process viewer) reads all processes via +;; exactly this interface set — mirror it. ps_all_states is the single +;; interface that covers the ~55 per-daemon dir/file /proc reads. LuCI also +;; execs a generic helper (exec.file) in-domain. + +(in luci + ;; (1) service/status pages + (call .initscript.read_file_files (subj)) + (call .initscript.search_file_dirs (subj)) + (call .conf.read_file_files (subj)) + + ;; (2) process/overview pages — mirror htop's read-all-processes set + (call .subj.ps_all_states (subj)) ;; read all domains' /proc state + (call .subj.signull_all_processes (subj)) ;; signal-0 liveness probes + (call .proc.list_fs_dirs (subj)) ;; read/open /proc itself + (call .exec.execute_file_files (subj))) ;; run a generic helper in-domain + +;; =========================================================================== +;; rpc.server (rpcd) — serving LuCI overview: confine helpers, read system state +;; =========================================================================== +;; Browsing LuCI's overview/status drives rpcd (rpc-sys / luci-rpc plugins) to +;; exec uci/ubus/nft/wget/jsonfilter/wg and gather system state. Confine the six +;; helpers in their own domains (transition-first): the transitions absorb each +;; tool's .exec.file read/map/execute_no_trans — so those are deliberately +;; NOT re-granted here. The rest are rpcd's own accesses: the netfilter netlink +;; socket (rpcd queries nftables via libnftnl directly, NOT through the nft +;; binary, so the nft transition does not cover it — hardware-confirmed), board/ +;; firmware/preinit/netif/modules state, /proc/uptime, generic data + conf, the +;; root dir, a lib-labelled helper exec, and log/run scratch dirs. + +(in rpc.server + (call .uci.subj_type_transition (subj)) + (call .ubus.subj_type_transition (subj)) + (call .nft.subj_type_transition (subj)) + (call .wget.subj_type_transition (subj)) + (call .jsonfilter.subj_type_transition (subj)) + (call .wireguard.wg.subj_type_transition (subj)) ;; luci wireguard status + + ;; rpcd talks to nftables over its own netfilter netlink socket (libnftnl) + (allow subj self create_netlink_netfilter_socket) + + ;; rpcd-native system-state reads (not attributable to a confined helper) + (call .firmware.data.list_file_dirs (subj)) + (call .preinit.data.list_file_dirs (subj)) + (call .preinit.data.read_file_files (subj)) + (call .netif.data.list_file_dirs (subj)) + (call .netif.data.read_file_files (subj)) + (call .modules.list_file_dirs (subj)) + (call .modules.read_file_files (subj)) + (call .uptime.read_procfile_files (subj)) + (call .root.list_file_dirs (subj)) + (call .data.read_file_files (subj)) + (call .conf.read_file_files (subj)) + (call .lib.execute_file_files (subj)) + + ;; log/run scratch dirs (rpcd stages output under /tmp/log + /tmp/run) + (call .logtmp.manage_file_dirs (subj)) + (call .runtmp.manage_file_dirs (subj)) + (call .runtmp.manage_file_files (subj))) + +;; =========================================================================== +;; wget / nft — silence inherited rpcd fd leaks (surfaced by the transitions) +;; =========================================================================== +;; Once rpcd confines wget/nft in their own domains (above), each tool inherits +;; rpcd's already-open fds on rpc.data / ucode.data files it never uses itself — +;; read-with-no-open leaks from the parent. dontaudit, mirroring how jsonfilter / +;; wireguard-tools already handle the same rpc.data/ucode.data leaks in dssp. + +(in wget + (call .rpc.data.dontaudit_readinherited_file_files (subj)) + (call .ucode.data.dontaudit_readinherited_file_files (subj))) + +(in nft + (call .rpc.data.dontaudit_readinherited_file_files (subj))) + +;; =========================================================================== +;; htop — interactive process viewer: read generic /etc config + /sys class/bus +;; =========================================================================== +;; htop (shipped admin tool) reads a generic /etc config (conf.file, beyond its +;; own htoprc), enumerates /sys/class and /sys/bus (device/sensor discovery), and +;; statfs's sysfs. Its stock domain grants /sys/block + /sys/devices but not +;; class/bus, generic conf, or the sysfs filesystem getattr — add those. + +(in htop + (call .conf.read_file_files (subj)) + (call .conf.list_file_dirs (subj)) + (call .class.list_sysfile_dirs (subj)) + (call .class.read_sysfile_lnk_files (subj)) + (call .bus.list_sysfile_dirs (subj)) + (call .bus.read_sysfile_lnk_files (subj)) + (call .sys.getattr_fs (subj))) + +;; =========================================================================== +;; cgiio (cgi-io) — LuCI firewall/realtime pages: query nftables via netlink +;; =========================================================================== +;; Browsing LuCI's firewall/realtime-status pages drives the cgi-io helper to run +;; a generic helper that reads nftables/conntrack state over a netfilter netlink +;; socket (needs net_admin). This is a distinct cgi-io use from the (now removed) +;; apk Software page; grant the netlink socket, the capability, and the in-domain +;; helper exec. + +(in cgiio + (allow subj self create_netlink_netfilter_socket) + (allow subj self (capability (net_admin))) + (call .exec.execute_file_files (subj)) + ;; connections/realtime page also reads /proc/net (conntrack, dev, …) + (call .net.read_procfile_files (subj)) + (call .net.read_procfile_lnk_files (subj)) + + ;; cgi-io's exec-streaming backend launches the LuCI status collectors — + ;; confine each in its own domain (transition-first). The transitions absorb + ;; each tool's own exec/data/runtmp reads (incl. the init scripts' functions/ + ;; uci/rc.common sourcing), so those are deliberately NOT granted to cgi-io. + (call .firewall.subj_type_transition (subj)) + (call .ucode.subj_type_transition (subj)) + (call .sys.initscript_subj_type_transition (subj)) + + ;; genuinely cgi-io's own: routing netlink, LuCI + libubox data it reads + ;; directly, /proc/version, /sys class/bus/devices, generic conf, /tmp/run + (allow subj self create_netlink_route_socket) + (call .luci.data.list_file_dirs (subj)) + (call .luci.data.read_file_files (subj)) + (call .libubox.data.list_file_dirs (subj)) + (call .libubox.data.read_file_files (subj)) + (call .conf.read_file_files (subj)) + (call .conf.list_file_dirs (subj)) + (call .version.read_procfile_files (subj)) + (call .devices.list_sysfile_dirs (subj)) + (call .devices.read_sysfile_files (subj)) + (call .module.list_sysfile_dirs (subj)) + (call .class.list_sysfile_dirs (subj)) + (call .class.read_sysfile_lnk_files (subj)) + (call .runtmp.manage_file_files (subj)) + (call .runtmp.manage_file_dirs (subj))) + +;; nlbwmon (bandwidth monitor) is a daemon with no subj_type_transition, so the +;; LuCI bandwidth page's cgi-io runs the nlbwmon query client IN-domain: exec the +;; binary, read its database, and connect to its control socket. +(in cgiio + (call .nlbwmon.exec.execute_file_files (subj)) + (call .nlbwmon.data.list_file_dirs (subj)) + (call .nlbwmon.data.read_file_files (subj)) + (call .nlbwmon.unix_stream_connect (subj))) + +;; wg (confined via the rpcd/cgiio transitions) inherits an rpcd rpc.data fd it +;; never uses itself — dontaudit, mirroring the wget/nft leaks below. +(in wireguard.wg + (call .rpc.data.dontaudit_readinherited_file_files (subj))) + +;; kmod loader, confined via the rpc.server config-page transition below, also +;; inherits an rpcd rpc.data fd it never uses — dontaudit, same leak pattern. +(in kmod.loader + (call .rpc.data.dontaudit_readinherited_file_files (subj))) + +;; =========================================================================== +;; rpc.server / cgiio — LuCI config-page rendering (Network/System/Services) +;; =========================================================================== +;; Loading the config-edit pages drives rpcd to read LuCI + generic data and the +;; apk repo config (attended-sysupgrade's update check), and to run the kmod +;; loader (confine it — absorbs the modules.file map); cgi-io reads shared /tmp. +;; View path only — Save&Apply's reload path shares these same domains. + +(in rpc.server + (call .luci.data.list_file_dirs (subj)) + (call .luci.data.read_file_files (subj)) + (call .data.list_file_dirs (subj)) + (call .apk.conf.read_file_files (subj)) + (call .apk.conf.list_file_dirs (subj)) + (call .tmp.manage_fs_files (subj)) + (call .kmod.loader.subj_type_transition (subj))) + +(in cgiio + (call .tmp.manage_fs_files (subj))) + +;; =========================================================================== +;; boarddetect — manage the /tmp tmpfs dir that holds board.json +;; =========================================================================== +;; Boot-time denials (pre-auditd, kernel-log only): dir tmp.fs +;; {write add_name remove_name} by comm=mv in boarddetect.subj. board_detect +;; stages /tmp/board.json through .board.json.tmp then `mv`s it into place. The +;; board.json / .board.json.tmp FILES are the private `board` type (boardfile.cil +;; filecon + name type_transition) and are already covered by the base domain's +;; .board.manage_file_files — but the /tmp directory they live in is the shared +;; tmp.fs type, which boarddetect could not add_name/remove_name. Grant the dir +;; analog of the tmp.manage_fs_files idiom (the same pairing sshsftp uses); the +;; files stay covered by .board, so no manage_fs_files is needed here. + +(in boarddetect + (call .tmp.manage_fs_dirs (subj))) + +;; =========================================================================== +;; fwtool — modify the sysupgrade staging tarball in /tmp +;; =========================================================================== +;; Sysupgrade-path denials (permissive, kernel-log during the flash): file +;; tmp.fs {write ioctl} by comm=fwtool on /tmp/mono-upgrade.*.tar.gz. fwtool +;; opens the upgrade tarball (created by the sysupgrade script as the shared +;; tmp.fs type), does a TIOCGWINSZ ioctl (0x5413) and strips/rewrites its +;; metadata trailer in place. It reads the file via the agent template but +;; could not write or ioctl the tmp.fs type. readwrite_fs_files is the narrow +;; fit — readwrite_file = {append getattr ioctl lock open read write}, exactly +;; the missing perms and no create/unlink (the script owns the file lifecycle). + +(in fwtool + (call .tmp.readwrite_fs_files (subj))) + +;; =========================================================================== +;; LuCI overview/status collectors — luci.peeraddr / cgiio / ubus / devstatus / +;; ipcalc +;; =========================================================================== +;; Rendering the LuCI Status overview drives rpcd + cgi-io to run the network +;; status collectors. Harvested (permissive) on a view path the base harvest +;; never exercised: +;; - luci.peeraddr runs `sort`, which probes its own scheduler (self getsched); +;; mirror the hotplug.call self-getsched grant above. +;; - cgi-io walks /etc/init.d collecting service state (search on the +;; initscript.file dirs) — the same interface luci uses above. +;; - ubus, exec'd under cgi-io's exec-streaming backend, writes its output back +;; to the inherited cgi-io pipe (cgiio.subj fifo). +;; - devstatus + ipcalc.sh, run by rpcd, use rpcd's inherited stdio fd and read +;; the LuCI ucode lib (sys.uc = ucode.data) plus the rpcd ucode plugins +;; (luci.upnp = rpc.data) — the same data the ucode / rpc.server domains +;; already read. + +(in luci.peeraddr + (allow subj self (process (getsched)))) + +(in cgiio + (call .initscript.search_file_dirs (subj))) + +(in ubus + (call .cgiio.readwriteinherited_subj_fifo_files (subj))) + +(in devstatus + (call .rpc.server.use_subj_fds (subj)) + (call .rpc.data.list_file_dirs (subj)) + (call .rpc.data.read_file_files (subj)) + (call .ucode.data.list_file_dirs (subj)) + (call .ucode.data.read_file_files (subj))) + +(in ipcalc + (call .rpc.server.use_subj_fds (subj)) + (call .rpc.data.list_file_dirs (subj)) + (call .rpc.data.read_file_files (subj)) + (call .ucode.data.list_file_dirs (subj)) + (call .ucode.data.read_file_files (subj))) + +;; rpcd also runs vnstat in-domain (no transition, like nlbwmon) for the LuCI +;; traffic graph — it reads the vnstat DB under /tmp/lib/vnstat (libtmp.file): +;; getattr/read/open/lock, all covered by read_file_files. +(in rpc.server + (call .libtmp.list_file_dirs (subj)) + (call .libtmp.read_file_files (subj))) + +;; LuCI Diagnostics page runs ping + traceroute in-domain under cgi-io (no dssp +;; domain to transition to). ping opens a raw ICMP socket (net_raw) + a udp +;; probe socket; traceroute drops privs (setuid/setgid) and opens raw + udp +;; sockets; both node_bind to net.netnode. Same cap + socket idiom the mtr +;; domain carries — create_{rawip,udp}_socket cover the create/rw/bind/connect/ +;; setopt/getattr perms harvested. +(in cgiio + (allow subj self (capability (net_raw setgid setuid))) + (allow subj self create_rawip_socket) + (allow subj self create_udp_socket) + (call .net.nodebind_netnode_rawip_sockets (subj)) + (call .net.nodebind_netnode_udp_sockets (subj))) + +;; =========================================================================== +;; netif.server (netifd) — create a tcp socket +;; =========================================================================== +;; netifd opens a tcp socket (a reachability/connectivity probe, surfaced by +;; the VLAN + bridge reconfiguration churn). Self-typed; only { create } was +;; harvested, but grant the create_tcp_socket classpermset (create + the +;; standard socket ops) so a follow-on connect/read does not resurface. + +(in netif.server + (allow subj self create_tcp_socket)) diff --git a/package/system/selinux-policy/files/cil/vwdnodedev.cil b/package/system/selinux-policy/files/cil/vwdnodedev.cil new file mode 100644 index 0000000000..183ae40952 --- /dev/null +++ b/package/system/selinux-policy/files/cil/vwdnodedev.cil @@ -0,0 +1,18 @@ +;; vwdnodedev.cil — private label for /dev/vwd0 +;; char dev (241,0) created at boot by a kernel module via devtmpfs: a virtual watchdog that cmm_t +;; pings via ioctl. Granted ONLY to cmm_t (read/open/ioctl, NO write). Mirrors cdxctrlnodedev.cil so +;; the node lands in its own vwd.nodedev type instead of falling to tmp.fs (which would leak every +;; unlabeled tmpfs char device to cmm). +(block vwd + + (filecon "/dev/vwd0" char nodedev_context) + + (macro tmp_fs_type_transition_nodedev ((type ARG1)) + (call .tmp.fs_type_transition + (ARG1 nodedev chr_file "vwd0"))) + + (blockinherit .nodedev.template)) + +(in nodedev.unconfined + + (call .vwd.tmp_fs_type_transition_nodedev (typeattr))) diff --git a/package/system/selinux-policy/files/selinux-config b/package/system/selinux-policy/files/selinux-config index ae5188a839..72385a0edb 100644 --- a/package/system/selinux-policy/files/selinux-config +++ b/package/system/selinux-policy/files/selinux-config @@ -1,2 +1,4 @@ -SELINUX=enforcing +# Bring-up: permissive so the first labeled boot collects AVCs instead of +# enforcing an incomplete policy. Flip to enforcing in P5 (after policy is done). +SELINUX=permissive SELINUXTYPE=selinux-policy diff --git a/target/linux/layerscape/base-files/etc/uci-defaults/99-mono-selinux-relabel b/target/linux/layerscape/base-files/etc/uci-defaults/99-mono-selinux-relabel new file mode 100644 index 0000000000..ab657b9398 --- /dev/null +++ b/target/linux/layerscape/base-files/etc/uci-defaults/99-mono-selinux-relabel @@ -0,0 +1,18 @@ +#!/bin/sh +# First-boot SELinux relabel for the Mono Gateway ext4 rootfs. +# +# The image rootfs is labeled at BUILD time (setfiles + mkfs.ext4 -d, see +# include/image.mk Image/mkfs/ext4), so daemons come up in their correct domains on the +# first boot with NO reboot needed. This pass only fixes files that arrive AFTER the +# image is built — chiefly config restored from sysupgrade.tgz, which busybox tar untars +# into / without security xattrs. Permissive during bring-up, so a missed label is +# logged, not fatal. Runs once (uci-defaults removes it on exit 0). + +[ -d /sys/fs/selinux ] || exit 0 +command -v restorecon >/dev/null 2>&1 || exit 0 + +# file_contexts is resolved from /etc/selinux/config's SELINUXTYPE by libselinux. +# No-op for the build-time-labeled image files; fixes restored config. +restorecon -RF / 2>/dev/null + +exit 0 diff --git a/target/linux/layerscape/image/armv8_64b.mk b/target/linux/layerscape/image/armv8_64b.mk index 10b91ea279..70c27010cb 100644 --- a/target/linux/layerscape/image/armv8_64b.mk +++ b/target/linux/layerscape/image/armv8_64b.mk @@ -413,6 +413,8 @@ define Build/mono-bootfs mkdir -p $@.bootdir/boot/extlinux $(CP) $(IMAGE_KERNEL) $@.bootdir/boot/Image.gz $(CP) $(DEVICE_DTS_DIR)/$(DEVICE_DTS).dtb $@.bootdir/boot/ + # SELinux mode is set by /etc/selinux/config (SELINUX=permissive during bring-up); + # a kernel `enforcing=` arg here is overridden at policy load, so it's not used. printf 'label OpenWrt\n\tkernel /boot/Image.gz\n\tfdt /boot/%s.dtb\n\tappend root=/dev/mmcblk0p2 rootwait console=ttyS0,115200 earlycon=uart8250,mmio,0x21c0500\n' \ "$(DEVICE_DTS)" > $@.bootdir/boot/extlinux/extlinux.conf # Backup GPT (33-sector tail) travels in the boot partition; first boot @@ -450,10 +452,11 @@ define Device/mono_gateway-dk DEVICE_MODEL := Gateway DK DEVICE_PACKAGES := kmod-ask-cdx kmod-ask-fci kmod-ask-auto-bridge \ cmm dpa-app fmc kmod-leds-lp5812 kmod-sfp-led fancontrol \ - luci-ssl luci-app-statistics luci-app-attendedsysupgrade \ + luci-light libustream-mbedtls px5g-mbedtls -luci-app-package-manager \ + luci-app-statistics luci-app-attendedsysupgrade \ kmod-wireguard wireguard-tools luci-proto-wireguard \ strongswan strongswan-default strongswan-mod-openssl openvpn-openssl luci-app-openvpn tailscale \ - sqm-scripts luci-app-sqm nlbwmon luci-app-nlbwmon \ + nlbwmon luci-app-nlbwmon \ adblock luci-app-adblock https-dns-proxy luci-app-https-dns-proxy \ banip luci-app-banip \ vnstat2 luci-app-vnstat2 htop iftop mtr tcpdump iperf3 \ @@ -464,7 +467,8 @@ define Device/mono_gateway-dk kmod-fs-vfat smartmontools usbutils pciutils i2c-tools \ tmux vim-full curl rsync jq less bind-dig openssh-sftp-server \ usign ca-bundle file ip-full resize2fs mono-update-check \ - cmmqos + cmmqos \ + policycoreutils-setfiles policycoreutils-sestatus KERNEL_NAME := Image KERNEL := kernel-bin | gzip FILESYSTEMS := ext4 diff --git a/target/linux/layerscape/image/gen_ext4_resizable.sh b/target/linux/layerscape/image/gen_ext4_resizable.sh new file mode 100755 index 0000000000..207bee5c8c --- /dev/null +++ b/target/linux/layerscape/image/gen_ext4_resizable.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# Creates an ext4 image with resize_inode support and proper GDT reservation +# Usage: gen_ext4_resizable.sh [reserved_pct] [journal|nojournal] [setfiles] +# Note: Uses mkfs.ext4 -d (e2fsprogs 1.43+) - no root required + +set -e + +OUTPUT="$1" +SIZE="$2" +BLOCKSIZE="$3" +SOURCE_DIR="$4" +RESERVED_PCT="${5:-0}" +JOURNAL="${6:-nojournal}" +SETFILES="${7:-}" + +TMPFILE="${OUTPUT}.tmp" +OWNER_CMDS="${OUTPUT}.owner.cmds" + +# Calculate actual content size + 50% headroom for filesystem overhead +CONTENT_SIZE=$(du -sb "$SOURCE_DIR" | cut -f1) +INITIAL_SIZE=$(( CONTENT_SIZE * 150 / 100 )) + +# Minimum 128MB to ensure proper metadata allocation +MIN_SIZE=$((128 * 1024 * 1024)) +if [ "$INITIAL_SIZE" -lt "$MIN_SIZE" ]; then + INITIAL_SIZE="$MIN_SIZE" +fi + +# Round up to 4K boundary +INITIAL_SIZE=$(( (INITIAL_SIZE + 4095) / 4096 * 4096 )) + +truncate -s "$INITIAL_SIZE" "$TMPFILE" + +# Create ext4 filesystem with: +# - resize_inode: enables online resize +# - resize=32G: reserves GDT blocks for future expansion to 32GB +# - has_journal per CONFIG_TARGET_EXT4_JOURNAL (rootfs is a writable ext4 on +# eMMC taking live UCI commits; without a journal an unclean power-off can +# corrupt it) +# +# When SELinux labels are requested, setfiles and mkfs.ext4 must run in the +# same fakeroot session so mkfs.ext4 can see the fake security.selinux xattrs. +if [ "$JOURNAL" = "journal" ]; then + FEATURES="resize_inode,has_journal" +else + FEATURES="resize_inode,^has_journal" +fi + +fakeroot -- sh -c ' +set -e +source_dir="$1" +setfiles="$2" +tmpfile="$3" +blocksize="$4" +reserved_pct="$5" +features="$6" + +if [ -n "$setfiles" ]; then + if [ ! -f "$source_dir/etc/selinux/config" ]; then + echo "SELinux labeling requested, but $source_dir/etc/selinux/config is missing" >&2 + exit 1 + fi + + . "$source_dir/etc/selinux/config" + file_contexts="$source_dir/etc/selinux/${SELINUXTYPE}/contexts/files/file_contexts" + if [ ! -f "$file_contexts" ]; then + echo "SELinux labeling requested, but $file_contexts is missing" >&2 + exit 1 + fi + + "$setfiles" -r "$source_dir" "$file_contexts" "$source_dir" +fi + +mkfs.ext4 -F -L rootfs -O "$features" \ + -E resize=34359738368 \ + -b "$blocksize" -m "$reserved_pct" \ + -d "$source_dir" \ + "$tmpfile" +' sh "$SOURCE_DIR" "$SETFILES" "$TMPFILE" "$BLOCKSIZE" "$RESERVED_PCT" "$FEATURES" + +# setfiles must run in fakeroot for SELinux xattrs, but that makes mkfs.ext4 +# record the build user's uid/gid. Normalize image ownership afterwards without +# changing the target rootfs tree on disk. +find "$SOURCE_DIR" -mindepth 1 -printf '%P\n' | while IFS= read -r path; do + escaped="${path//\\/\\\\}" + escaped="${escaped//\"/\\\"}" + printf 'set_inode_field "/%s" uid 0\n' "$escaped" + printf 'set_inode_field "/%s" gid 0\n' "$escaped" +done > "$OWNER_CMDS" +debugfs -w -f "$OWNER_CMDS" "$TMPFILE" >/dev/null 2>&1 +rm -f "$OWNER_CMDS" + +# Shrink filesystem to minimum +e2fsck -fy "$TMPFILE" >/dev/null 2>&1 || true +resize2fs -M "$TMPFILE" >/dev/null 2>&1 + +# Get actual filesystem size and add padding for kernel.itb (added later by mono-add-kernel) +# Need at least 32MB extra for kernel + headroom +BLOCKS=$(dumpe2fs -h "$TMPFILE" 2>/dev/null | grep "Block count:" | awk '{print $3}') +BLOCK_SIZE_ACTUAL=$(dumpe2fs -h "$TMPFILE" 2>/dev/null | grep "Block size:" | awk '{print $3}') +FS_SIZE=$(( BLOCKS * BLOCK_SIZE_ACTUAL )) +PADDING=$(( FS_SIZE * 5 / 100 )) # 5% padding +MIN_PADDING=$((32 * 1024 * 1024)) # minimum 32MB for kernel +if [ "$PADDING" -lt "$MIN_PADDING" ]; then + PADDING="$MIN_PADDING" +fi +FINAL_SIZE=$(( FS_SIZE + PADDING )) + +# Truncate file to final size +truncate -s "$FINAL_SIZE" "$TMPFILE" + +# Resize filesystem to fill the truncated file +e2fsck -fy "$TMPFILE" >/dev/null 2>&1 || true +resize2fs "$TMPFILE" >/dev/null 2>&1 + +mv "$TMPFILE" "$OUTPUT" -- 2.47.3