From e94eae637413c9e9355fb37516b6df8b02457c2d Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Fri, 24 Apr 2026 19:48:38 +0800 Subject: [PATCH 01/17] Makefile: add setcap target for rootless uspace builds New 'setcap' target grants rtapi_app the kernel privileges it needs via file capabilities instead of setuid root: cap_ipc_lock, cap_net_admin, cap_sys_rawio, cap_sys_nice Clears the setuid bit first so a prior 'make setuid' run cannot silently combine with caps. Post-build warning at the end of 'make build-software' now treats either setuid-root OR any file capabilities as "ready to run", and suggests both 'sudo make setuid' and 'sudo make setcap' when neither is set. --- src/Makefile | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Makefile b/src/Makefile index c564f77e843..45354ba4349 100644 --- a/src/Makefile +++ b/src/Makefile @@ -57,7 +57,7 @@ endif ifeq ($(MAKECMDGOALS),) TRIVIAL_BUILD=no else -ifeq ($(filter-out docclean clean setuid install tags swish,$(MAKECMDGOALS)),) +ifeq ($(filter-out docclean clean setuid setcap install tags swish,$(MAKECMDGOALS)),) TRIVIAL_BUILD=yes else TRIVIAL_BUILD=no @@ -140,7 +140,13 @@ ifeq ($(RUN_IN_PLACE),yes) ifneq ($(BUILD_SYS),uspace) @if [ -f ../bin/linuxcnc_module_helper ]; then if ! [ `id -u` = 0 -a -O ../bin/linuxcnc_module_helper -a -u ../bin/linuxcnc_module_helper ]; then $(VECHO) "You now need to run 'sudo make setuid' in order to run in place."; fi; fi else - @if [ -f ../bin/rtapi_app ]; then if ! [ `id -u` = 0 -a -O ../bin/rtapi_app -a -u ../bin/rtapi_app ]; then $(VECHO) "You now need to run 'sudo make setuid' in order to run in place with access to hardware."; fi; fi + @if [ -f ../bin/rtapi_app ]; then \ + if [ `id -u` = 0 -a -O ../bin/rtapi_app -a -u ../bin/rtapi_app ]; then :; \ + elif PATH="/sbin:/usr/sbin:$$PATH" command -v getcap >/dev/null 2>&1 \ + && [ -n "`PATH=/sbin:/usr/sbin:$$PATH getcap ../bin/rtapi_app 2>/dev/null`" ]; then :; \ + else $(VECHO) "You now need to run 'sudo make setuid' or 'sudo make setcap' in order to run in place with access to hardware."; \ + fi; \ + fi endif endif @@ -568,6 +574,22 @@ endif chown root ../bin/linuxcnc_module_helper chmod 4750 ../bin/linuxcnc_module_helper +# File capabilities alternative to setuid (uspace only). +# Grants rtapi_app the kernel privileges it needs without running as root: +# cap_ipc_lock - mlock() for realtime memory +# cap_net_admin - raw socket access for hm2_eth / iptables management +# cap_sys_rawio - iopl() and /dev/mem for parallel port and PCI I/O +# cap_sys_nice - SCHED_FIFO scheduling and CPU affinity +# Clears any setuid bit left by a prior 'make setuid' so the two paths don't +# silently stack. +setcap: +ifeq ($(BUILD_SYS),uspace) + chmod u-s ../bin/rtapi_app + setcap cap_ipc_lock,cap_net_admin,cap_sys_rawio,cap_sys_nice+ep ../bin/rtapi_app +else + @echo "setcap target is only supported for uspace builds" >&2; exit 1 +endif + # These rules allows a header file from this directory to be installed into # ../include. A pair of rules like these will exist in the Submakefile # of each file that contains headers. From 9f5ccf8f4665f7208066152dbc700f6b897680d1 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Fri, 24 Apr 2026 20:43:02 +0800 Subject: [PATCH 02/17] hm2_eth: skip iptables setup when unprivileged Running rtapi_app with file capabilities (via 'make setcap') rather than setuid root means iptables commands can no longer be exec'd: Linux capabilities do not propagate across exec() into the iptables binary, so CAP_NET_ADMIN on rtapi_app is dropped before iptables starts. Two complementary changes handle this cleanly: 1. Auto-detect at module load. A read-only 'iptables -L INPUT' probe runs once the first time use_iptables() is consulted. If it fails (missing binary, permission denial, no shell access) we log a single informational message and mark iptables as unavailable for the rest of the session. No more noisy "ERROR: Failed to create iptables chain" on every rootless module load. 2. Explicit override. New module parameter 'no_iptables=1' disables all iptables interaction even when the probe would succeed, for administrators managing the firewall externally (nftables, firewalld, systemd units). install_iptables_perinterface() gains a use_iptables() gate that was previously missing, bringing it in line with the other iptables call sites. Manpage updated to document the new parameter and the auto-detect behaviour. --- docs/src/man/man9/hm2_eth.9.adoc | 13 ++++++++++++- src/hal/drivers/mesa-hostmot2/hm2_eth.c | 22 +++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/src/man/man9/hm2_eth.9.adoc b/docs/src/man/man9/hm2_eth.9.adoc index 7cf1b3015a9..8627786a93c 100644 --- a/docs/src/man/man9/hm2_eth.9.adoc +++ b/docs/src/man/man9/hm2_eth.9.adoc @@ -7,7 +7,7 @@ IO boards, with HostMot2 firmware. == SYNOPSIS -*loadrt hm2_eth* [**config=**"__str__[,__str__...]"] [**board_ip=**__ip__[,__ip__...] ] [**board_mac=**__mac__[,__mac__...] ] +*loadrt hm2_eth* [**config=**"__str__[,__str__...]"] [**board_ip=**__ip__[,__ip__...] ] [**board_mac=**__mac__[,__mac__...] ] [**no_iptables=**__0|1__] ____ *config* [default: ""]:: @@ -15,6 +15,17 @@ ____ *board_ip* [default: ""]:: The IP address of the board(s), separated by commas. As shipped, the board address is 192.168.1.121. +*no_iptables* [default: 0]:: + Explicit override that disables all iptables interaction. By default + hm2_eth probes iptables at load time with a read-only listing and + silently skips rule installation if the probe fails (for example + when rtapi_app is running unprivileged via file capabilities rather + than setuid root, since Linux capabilities are not inherited across + exec() into the iptables binary). Set *no_iptables=1* when iptables + is reachable but you prefer to manage the firewall externally + (nftables, firewalld, systemd units). In both cases the + interface-isolation rules must be provided by the administrator; see + the NOTES section below. ____ == DESCRIPTION diff --git a/src/hal/drivers/mesa-hostmot2/hm2_eth.c b/src/hal/drivers/mesa-hostmot2/hm2_eth.c index 8cb23fe40b7..914df11cd2d 100644 --- a/src/hal/drivers/mesa-hostmot2/hm2_eth.c +++ b/src/hal/drivers/mesa-hostmot2/hm2_eth.c @@ -97,6 +97,9 @@ RTAPI_MP_ARRAY_STRING(config, MAX_ETH_BOARDS, "config string for the AnyIO board int debug = 0; RTAPI_MP_INT(debug, "Developer/debug use only! Enable debug logging."); +static int no_iptables = 0; +RTAPI_MP_INT(no_iptables, "Skip automatic iptables rule installation; firewall must be configured externally."); + static int boards_count = 0; int comm_active = 0; @@ -475,6 +478,22 @@ static bool chain_exists() { static int iptables_state = -1; static bool use_iptables() { if(iptables_state == -1) { + if(no_iptables) { + LL_PRINT("Skipping iptables setup (no_iptables=1); configure firewall externally.\n"); + return (iptables_state = 0); + } + // Pre-flight probe: capabilities held by rtapi_app do not propagate + // across exec(), so when we run unprivileged (file-caps instead of + // setuid root) iptables cannot touch the kernel tables. A read-only + // list of a built-in chain is the cheapest way to find out without + // leaving side effects. + if(shell(IPTABLES " -n -L INPUT > /dev/null 2>&1") != EXIT_SUCCESS) { + LL_PRINT("iptables is not available to this process " + "(running unprivileged?); skipping automatic rule " + "installation. Configure firewall externally to " + "isolate the hm2-eth interface.\n"); + return (iptables_state = 0); + } if(!chain_exists()) { int res = shell(IPTABLES " -N " CHAIN); if(res != EXIT_SUCCESS) { @@ -1595,7 +1614,8 @@ int rtapi_app_main(void) { if(!added) goto error; if(*added) continue; - install_iptables_perinterface(ifptr); + if(use_iptables()) + install_iptables_perinterface(ifptr); *added = 1; } From 49523cddb71afec552920431910bcf12f38a8ed7 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Fri, 24 Apr 2026 20:21:23 +0800 Subject: [PATCH 03/17] rtapi: make rtapi_is_realtime() a SCHED_FIFO success probe Issue #3928 reported three bugs in rtapi_is_realtime(): 1. It required a setuid bit on EMC2_BIN_DIR/rtapi_app, ignoring file capabilities that grant the same kernel privileges. 2. It stat()ed a fixed path instead of the running binary, so wrapper-based installs (NixOS /run/wrappers and similar) never saw the check succeed. 3. It ran the setuid test before LINUXCNC_FORCE_REALTIME, silently discarding the environment variable. PR #918 replaced the whole function with 'return 1', which breaks the sim-vs-rt distinction that GUIs read via hal.is_sim / hal.is_rt and removes the makeApp() sim fallback. Rework rtapi_is_realtime() as a runtime capability probe: briefly set SCHED_FIFO on the calling thread and restore the previous policy, cache the result. LINUXCNC_FORCE_REALTIME short-circuits before the probe, and the RTAI / Xenomai backend detectors still force-true when those environments are present. This matches the convention used by comparable userspace-realtime projects (JACK's jack_is_realtime, PipeWire, rtkit, Xenomai, Klipper): surface observed capability rather than kernel metadata, and let callers who need EPERM visibility use the action API directly. makeApp() drops its own probe and calls rtapi_is_realtime() to choose between SCHED_FIFO and SCHED_OTHER, falling back to POSIX non-realtime with a warning that points at 'make setcap' or 'make setuid'. detect_preempt_rt() is removed -- the probe subsumes it and also works on kernels that no longer expose /sys/kernel/realtime (6.12+). --- src/rtapi/uspace_common.h | 71 +++++++++++++++++++--------------- src/rtapi/uspace_rtapi_main.cc | 10 ++++- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/src/rtapi/uspace_common.h b/src/rtapi/uspace_common.h index 5b625ff03bc..0e76d7e39ec 100644 --- a/src/rtapi/uspace_common.h +++ b/src/rtapi/uspace_common.h @@ -27,6 +27,8 @@ #include #include #include +#include +#include #include #include @@ -351,25 +353,6 @@ int rtapi_exit(int module_id) } int rtapi_is_kernelspace() { return 0; } -static int _rtapi_is_realtime = -1; -#ifdef __linux__ -static int detect_preempt_rt() { - struct utsname u; - int crit1 = 0; - - uname(&u); - crit1 = strcasestr (u.version, "PREEMPT RT") != 0; - - //"PREEMPT_RT" is used in the version string instead of "PREEMPT RT" starting with kernel version 5.4 - crit1 = crit1 || (strcasestr(u.version, "PREEMPT_RT") != 0); - - return crit1; -} -#else -static int detect_preempt_rt() { - return 0; -} -#endif #ifdef USPACE_RTAI static int detect_rtai() { struct utsname u; @@ -404,22 +387,48 @@ static int detect_xenomai_evl() { } #endif -static int detect_env_override() { - char *p = getenv("LINUXCNC_FORCE_REALTIME"); - return p != NULL && atoi(p) != 0; -} - -static int detect_realtime() { - struct stat st; - if ((stat(EMC2_BIN_DIR "/rtapi_app", &st) < 0) - || st.st_uid != 0 || !(st.st_mode & S_ISUID)) +// Success-probe for realtime scheduling: briefly try to set SCHED_FIFO on +// the calling thread and restore the previous policy. Succeeds when the +// process holds CAP_SYS_NICE (file caps or setuid root) or has a matching +// RLIMIT_RTPRIO. Works on any kernel, so the probe also covers the +// PREEMPT_RT-vs-stock distinction implicitly: if we can actually get +// SCHED_FIFO, the platform can deliver realtime, regardless of how. +static int can_set_sched_fifo(void) { + struct sched_param old_param, probe_param; + int old_policy = sched_getscheduler(0); + if(old_policy < 0) return 0; + if(sched_getparam(0, &old_param) < 0) return 0; + + memset(&probe_param, 0, sizeof(probe_param)); + probe_param.sched_priority = sched_get_priority_min(SCHED_FIFO); + if(sched_setscheduler(0, SCHED_FIFO, &probe_param) < 0) return 0; - return detect_env_override() || detect_preempt_rt() || detect_rtai() || detect_xenomai() || detect_xenomai_evl(); + + // Best-effort restore; if this fails we are still on SCHED_FIFO at + // minimum priority, which is no worse than where we started. + sched_setscheduler(0, old_policy, &old_param); + return 1; } +// rtapi_is_realtime() reports whether this process can actually run +// realtime code. This matches the convention used by JACK, PipeWire, +// rtkit, Xenomai, and Klipper: surface the observed capability, not +// kernel metadata. The old setuid-root stat check has been removed; it +// stat()ed EMC2_BIN_DIR/rtapi_app rather than the running binary (breaking +// wrapper-based installs like NixOS /run/wrappers) and silently masked +// LINUXCNC_FORCE_REALTIME (see issue #3928). int rtapi_is_realtime() { - if(_rtapi_is_realtime == -1) _rtapi_is_realtime = detect_realtime(); - return _rtapi_is_realtime; + static int cached = -1; + if(cached != -1) return cached; + + const char *force = getenv("LINUXCNC_FORCE_REALTIME"); + if(force != NULL && atoi(force) != 0) + return (cached = 1); + + if(detect_rtai() || detect_xenomai() || detect_xenomai_evl()) + return (cached = 1); + + return (cached = can_set_sched_fifo()); } /* Like clock_nanosleep, except that an optional 'estimate of now' parameter may diff --git a/src/rtapi/uspace_rtapi_main.cc b/src/rtapi/uspace_rtapi_main.cc index 3a73c5e294e..edeaab17adb 100644 --- a/src/rtapi/uspace_rtapi_main.cc +++ b/src/rtapi/uspace_rtapi_main.cc @@ -987,7 +987,15 @@ static RtapiApp *makeDllApp(const std::string &dllName, int policy) { static RtapiApp *makeApp() { RtapiApp *app; - if (WithRoot::getEuid() != 0 || harden_rt() < 0) { + bool rt_ok = rtapi_is_realtime(); + if (!rt_ok) { + rtapi_print_msg(RTAPI_MSG_ERR, + "Note: SCHED_FIFO not permitted for this process, " + "falling back to POSIX non-realtime. " + "Run 'sudo make setcap' (preferred) or 'sudo make setuid' " + "on rtapi_app to enable realtime scheduling.\n"); + } + if (!rt_ok || harden_rt() < 0) { app = makeDllApp("liblinuxcnc-uspace-posix.so.0", SCHED_OTHER); } else { WithRoot r; From e42410aa79c43a8a96ee0bc10764ccdfff74e91d Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Fri, 24 Apr 2026 20:26:51 +0800 Subject: [PATCH 04/17] udev: rules for rootless Mesa HostMot2 PCI and /dev/cpu_dma_latency Two new rule files installed into /usr/lib/udev/rules.d via debian/extras. Needed only when rtapi_app runs under file capabilities (from 'make setcap'); setuid-root builds bypass DAC regardless. 99-linuxcnc-hm2-pci.rules -- chgrp+chmod PCI config space and resource BARs to the plugdev group for direct-Mesa (vendor 0x2718) cards and for the PLX-bridged Mesa board family (0x10B5 + Mesa subsystem_device ids drawn from hm2_pci.h). 99-linuxcnc-realtime.rules -- chmod /dev/cpu_dma_latency to mode 0660, group plugdev, so harden_rt() can pin the CPU idle state without CAP_DAC_OVERRIDE. plugdev is already the default hotplug group on Debian-derived distros and matches the convention used by the existing Shuttle/XHC rule files. --- .../udev/rules.d/99-linuxcnc-hm2-pci.rules | 51 +++++++++++++++++++ .../udev/rules.d/99-linuxcnc-realtime.rules | 8 +++ 2 files changed, 59 insertions(+) create mode 100644 debian/extras/lib/udev/rules.d/99-linuxcnc-hm2-pci.rules create mode 100644 debian/extras/lib/udev/rules.d/99-linuxcnc-realtime.rules diff --git a/debian/extras/lib/udev/rules.d/99-linuxcnc-hm2-pci.rules b/debian/extras/lib/udev/rules.d/99-linuxcnc-hm2-pci.rules new file mode 100644 index 00000000000..5bf292b4beb --- /dev/null +++ b/debian/extras/lib/udev/rules.d/99-linuxcnc-hm2-pci.rules @@ -0,0 +1,51 @@ +# LinuxCNC - grant Mesa HostMot2 PCI cards to the plugdev group so +# rtapi_app can map their config space and BARs without CAP_DAC_OVERRIDE. +# Needed only for rootless (file-capabilities) builds; setuid-root builds +# bypass DAC regardless. +# +# Add or remove Mesa subsystem IDs below to match new boards. SSIDs come +# from src/hal/drivers/mesa-hostmot2/hm2_pci.h. + +ACTION!="add|change", GOTO="linuxcnc_hm2_end" +SUBSYSTEM!="pci", GOTO="linuxcnc_hm2_end" + +# Direct-Mesa cards (5i24, 5i25, 5i25T, 6i25, 6i25T): single vendor match. +ATTR{vendor}=="0x2718", GOTO="linuxcnc_hm2_chmod" + +# PLX-bridged Mesa cards share vendor 0x10B5 with generic PLX bridges, +# so match Mesa subsystem_device ids one at a time. +ATTR{vendor}!="0x10b5", GOTO="linuxcnc_hm2_end" +# 5i20 +ATTRS{subsystem_device}=="0x3131", GOTO="linuxcnc_hm2_chmod" +# 4i65 +ATTRS{subsystem_device}=="0x3132", GOTO="linuxcnc_hm2_chmod" +# 4i68 (old SSID) +ATTRS{subsystem_device}=="0x3133", GOTO="linuxcnc_hm2_chmod" +# 4i68 (new SSID) +ATTRS{subsystem_device}=="0x3311", GOTO="linuxcnc_hm2_chmod" +# 5i21 +ATTRS{subsystem_device}=="0x3312", GOTO="linuxcnc_hm2_chmod" +# 5i22-1.5M +ATTRS{subsystem_device}=="0x3313", GOTO="linuxcnc_hm2_chmod" +# 5i22-1.0M +ATTRS{subsystem_device}=="0x3314", GOTO="linuxcnc_hm2_chmod" +# 5i23 +ATTRS{subsystem_device}=="0x3315", GOTO="linuxcnc_hm2_chmod" +# 3x20-10 +ATTRS{subsystem_device}=="0x3427", GOTO="linuxcnc_hm2_chmod" +# 3x20-15 +ATTRS{subsystem_device}=="0x3428", GOTO="linuxcnc_hm2_chmod" +# 3x20-20 +ATTRS{subsystem_device}=="0x3429", GOTO="linuxcnc_hm2_chmod" +# 4i69-16 +ATTRS{subsystem_device}=="0x3472", GOTO="linuxcnc_hm2_chmod" +# 4i69-25 +ATTRS{subsystem_device}=="0x3473", GOTO="linuxcnc_hm2_chmod" +GOTO="linuxcnc_hm2_end" + +LABEL="linuxcnc_hm2_chmod" +# Fork a helper; sysfs files may not exist until the device is fully +# sized, so the chmod failures are ignored. +RUN+="/bin/sh -c 'chgrp plugdev /sys%p/config /sys%p/resource* 2>/dev/null; chmod g+rw /sys%p/config /sys%p/resource* 2>/dev/null; exit 0'" + +LABEL="linuxcnc_hm2_end" diff --git a/debian/extras/lib/udev/rules.d/99-linuxcnc-realtime.rules b/debian/extras/lib/udev/rules.d/99-linuxcnc-realtime.rules new file mode 100644 index 00000000000..fa3b057e192 --- /dev/null +++ b/debian/extras/lib/udev/rules.d/99-linuxcnc-realtime.rules @@ -0,0 +1,8 @@ +# LinuxCNC - expose realtime tuning knobs to the plugdev group so that +# rtapi_app can tune latency without CAP_DAC_OVERRIDE when running under +# file capabilities. setuid-root builds do not need this rule. + +# /dev/cpu_dma_latency: harden_rt() opens this to pin CPU idle states at +# C0, cutting wake-up jitter on AC-powered machines. Default is 0600 +# root:root, so an unprivileged rtapi_app would fail to open it. +KERNEL=="cpu_dma_latency", MODE="0660", GROUP="plugdev" From 35aa9c116917244e0a17c68bfa9acfd5abd597ca Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:44:39 +0800 Subject: [PATCH 05/17] rtapi: always attempt mlockall, do not gate on rlim_max PR #918's SCHED_FIFO unification commit replaced setrlimit(RLIMIT_MEMLOCK, unlimited) + unconditional mlockall() with getrlimit + mlockall only when rlim_max >= 2 * PRE_ALLOC_SIZE. This silently skips mlockall inside Debian-packaging CI containers where the default RLIMIT_MEMLOCK is 64 KiB, because 64 KiB is less than the 64 MiB threshold. Without locked pages, thread scheduling jitter causes tests/threads.0 to miss counter increments. Seen as 'line 3097: got 0, expected 10 or 1' on amd64 trixie and sid. Always call mlockall and log failures. A best-effort setrlimit is still attempted (to raise the soft cap to the hard cap without requiring CAP_SYS_RESOURCE), but the mlockall call itself succeeds on any kernel as long as the process has CAP_IPC_LOCK, regardless of the rlimit. --- src/rtapi/uspace_rtapi_main.cc | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/rtapi/uspace_rtapi_main.cc b/src/rtapi/uspace_rtapi_main.cc index edeaab17adb..3e1f61caed3 100644 --- a/src/rtapi/uspace_rtapi_main.cc +++ b/src/rtapi/uspace_rtapi_main.cc @@ -840,13 +840,23 @@ static void signal_handler(int sig, siginfo_t * /*si*/, void * /*uctx*/) { const static size_t PRE_ALLOC_SIZE = 1024 * 1024 * 32; const static struct rlimit unlimited = {RLIM_INFINITY, RLIM_INFINITY}; static void configure_memory() { - int res = setrlimit(RLIMIT_MEMLOCK, &unlimited); - if (res < 0) - perror("setrlimit"); + // Best-effort raise of the soft cap to the hard cap. Fails on + // unprivileged processes without CAP_SYS_RESOURCE or without a + // matching setrlimit; CAP_IPC_LOCK alone lets mlockall succeed + // regardless of the rlimit, so ignoring the failure is safe. + struct rlimit limit; + if (getrlimit(RLIMIT_MEMLOCK, &limit) == 0) { + limit.rlim_cur = limit.rlim_max; + if (setrlimit(RLIMIT_MEMLOCK, &limit) < 0) + rtapi_print_msg(RTAPI_MSG_DBG, + "setrlimit(RLIMIT_MEMLOCK) failed: %s\n", strerror(errno)); + } - res = mlockall(MCL_CURRENT | MCL_FUTURE); + int res = mlockall(MCL_CURRENT | MCL_FUTURE); if (res < 0) - perror("mlockall"); + rtapi_print_msg(RTAPI_MSG_WARN, + "mlockall failed: %s. Realtime latency may suffer.\n", + strerror(errno)); #ifdef __linux__ /* Turn off malloc trimming.*/ From 900bb13a1d719ffda3ced23d8270b11de7f7a337 Mon Sep 17 00:00:00 2001 From: Damian Wrobel Date: Sat, 23 May 2020 12:21:46 +0200 Subject: [PATCH 06/17] Use TEMP_FAILURE_RETRY() to handle EINTR properly Signed-off-by: Damian Wrobel --- src/rtapi/uspace_common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rtapi/uspace_common.h b/src/rtapi/uspace_common.h index 0e76d7e39ec..796fae0dfd5 100644 --- a/src/rtapi/uspace_common.h +++ b/src/rtapi/uspace_common.h @@ -442,7 +442,7 @@ static int rtapi_clock_nanosleep(clockid_t clock_id, int flags, { (void)pnow; #if defined(HAVE_CLOCK_NANOSLEEP) - return clock_nanosleep(clock_id, flags, prequest, remain); + return TEMP_FAILURE_RETRY(clock_nanosleep(clock_id, flags, prequest, remain)); #else if(flags == 0) return nanosleep(prequest, remain); From 9762fad6d2d4ee6cc5c7366f23991586a1dde3e0 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:44:03 +0800 Subject: [PATCH 07/17] rtapi: make harden_rt() rootless-safe setrlimit(RLIMIT_RTPRIO, RLIM_INFINITY) requires CAP_SYS_RESOURCE, which neither setuid root nor 'make setcap' (CAP_IPC_LOCK, CAP_NET_ADMIN, CAP_SYS_RAWIO, CAP_SYS_NICE) grants by default. Under rootless the call returns EPERM, which the previous code treated as fatal: harden_rt() returned -errno, makeApp() fell back to SCHED_OTHER, and the SCHED_FIFO probe in rtapi_is_realtime() became a lie. Soften both setrlimit calls to best-effort. SCHED_FIFO scheduling itself only needs CAP_SYS_NICE, which the cap set does grant; the rlimit just bounds the achievable priority. Distros that want unlimited RT priority can ship a /etc/security/limits.d entry, or the operator can grant CAP_SYS_RESOURCE explicitly. Also update the iopl() error message: 'sudo make setuid' is no longer the only path, and the diagnostic should name the missing capability (CAP_SYS_RAWIO). Derived from Damian Wrobel's 2020 'Unify FIFO_SCHED between root and non-root user' commit, ported onto hdiethelm's rtapi cleanup v2 structure. --- src/rtapi/uspace_rtapi_main.cc | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/rtapi/uspace_rtapi_main.cc b/src/rtapi/uspace_rtapi_main.cc index 3e1f61caed3..450e625fb91 100644 --- a/src/rtapi/uspace_rtapi_main.cc +++ b/src/rtapi/uspace_rtapi_main.cc @@ -910,7 +910,7 @@ static int harden_rt() { RTAPI_MSG_ERR, "iopl() failed: %s\n" "cannot gain I/O privileges - " - "forgot 'sudo make setuid' or using secure boot? -" + "missing CAP_SYS_RAWIO or using secure boot? - " "parallel port access is not allowed\n", strerror(errno) ); @@ -919,19 +919,21 @@ static int harden_rt() { struct sigaction sig_act = {}; #ifdef __linux__ - // enable realtime - if (setrlimit(RLIMIT_RTPRIO, &unlimited) < 0) { - rtapi_print_msg(RTAPI_MSG_WARN, "setrlimit(RTLIMIT_RTPRIO): %s\n", strerror(errno)); - return -errno; - } + // Best-effort raise of RTPRIO/CORE soft caps. Setting these to + // RLIM_INFINITY requires CAP_SYS_RESOURCE, which neither setuid root + // nor file capabilities grant by default. Without it, threads still + // get SCHED_FIFO via CAP_SYS_NICE; the rlimit just gates how high + // they can go. Don't fail harden_rt() when it can't be raised. + if (setrlimit(RLIMIT_RTPRIO, &unlimited) < 0) + rtapi_print_msg(RTAPI_MSG_DBG, + "setrlimit(RLIMIT_RTPRIO): %s\n", strerror(errno)); - // enable core dumps if (setrlimit(RLIMIT_CORE, &unlimited) < 0) rtapi_print_msg( RTAPI_MSG_WARN, "setrlimit: %s - core dumps may be truncated or non-existent\n", strerror(errno) ); - // even when setuid root + // even when running with elevated capabilities if (prctl(PR_SET_DUMPABLE, 1) < 0) rtapi_print_msg( RTAPI_MSG_WARN, From 5257ad175f7a85be32137828a0ac8741702dab82 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sat, 25 Apr 2026 19:23:49 +0800 Subject: [PATCH 08/17] rtapi: warn on stock kernel, gate Xenomai/RTAI on setuid Address two review concerns from @hdiethelm on PR #3964: 1. SCHED_FIFO probe was too generous on PREEMPT_DYNAMIC stock kernels. The probe correctly reports that SCHED_FIFO is achievable, but the resulting latency on a non-PREEMPT_RT kernel can be tens of milliseconds, surprising users who read 'POSIX realtime' and expect bounded scheduling. Restore detect_preempt_rt() (uname-based) and emit a one-shot warning at makeApp() when the SCHED_FIFO path is chosen but the kernel lacks PREEMPT_RT and there is no Xenomai/RTAI backend. Behavior is unchanged: SCHED_FIFO on stock is still strictly better than SCHED_OTHER, the warning just makes the tradeoff visible. 2. Xenomai/RTAI backends still need root for iopl() (RTAI) or RTDM device access (Xenomai/EVL) and were being selected for unprivileged users on a Xenomai kernel, leading to 'iopl() failed: Operation not permitted'. Gate detect_rtai/detect_xenomai/detect_xenomai_evl on geteuid()==0 so unprivileged callers fall through to the SCHED_FIFO probe and a clean POSIX path. This is a band-aid pending proper capability/group support (udev rules + 'xenomai'/'evl' group membership, the approach Xenomai's own docs recommend); marked with a FIXME pointing at @hdiethelm's planned follow-up. The probe-based rtapi_is_realtime() itself is unchanged: it remains a pure capability check, matching the convention in JACK, PipeWire, rtkit, and Klipper. Kernel quality is reported as a separate diagnostic, not folded into the boolean. --- src/rtapi/uspace_common.h | 40 ++++++++++++++++++++++++++++++++++ src/rtapi/uspace_rtapi_main.cc | 11 ++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/rtapi/uspace_common.h b/src/rtapi/uspace_common.h index 796fae0dfd5..8ee75734841 100644 --- a/src/rtapi/uspace_common.h +++ b/src/rtapi/uspace_common.h @@ -353,8 +353,46 @@ int rtapi_exit(int module_id) } int rtapi_is_kernelspace() { return 0; } + +#ifdef __linux__ +// detect_preempt_rt() inspects uname for the PREEMPT_RT marker. Used only +// for diagnostic warning at startup; callers must not gate behavior on +// the kernel string, since SCHED_FIFO on a PREEMPT_DYNAMIC kernel is still +// useful (better than SCHED_OTHER, worse than PREEMPT_RT). +// Marked unused because uspace_common.h is also included from ULAPI TUs +// that do not reference it. +__attribute__((unused)) +static int detect_preempt_rt() { + struct utsname u; + if(uname(&u) < 0) return 0; + return strcasestr(u.version, "PREEMPT RT") != 0 + || strcasestr(u.version, "PREEMPT_RT") != 0; +} +#else +__attribute__((unused)) +static int detect_preempt_rt() { + return 0; +} +#endif + +// FIXME: detect_rtai/detect_xenomai/detect_xenomai_evl currently gate on +// setuid because the RTAI/Xenomai backends still need root for iopl() +// (RTAI) or RTDM device access (Xenomai/EVL). Long-term these should +// probe the actual capability the way can_set_sched_fifo() does, paired +// with udev rules + a 'xenomai'/'evl' group; @hdiethelm has a follow-up +// planned. Until then, an unprivileged user on a Xenomai kernel cannot +// claim the Xenomai backend, and falls back to the SCHED_FIFO probe. +// Marked unused because the helper is called only when one of the +// USPACE_RTAI / USPACE_XENOMAI / USPACE_XENOMAI_EVL macros is defined, +// and the header is included from ULAPI TUs that define none of them. +__attribute__((unused)) +static int has_setuid_root() { + return geteuid() == 0; +} + #ifdef USPACE_RTAI static int detect_rtai() { + if(!has_setuid_root()) return 0; struct utsname u; uname(&u); return strcasestr (u.release, "-rtai") != 0; @@ -366,6 +404,7 @@ static int detect_rtai() { #endif #ifdef USPACE_XENOMAI static int detect_xenomai() { + if(!has_setuid_root()) return 0; struct stat sb; //Running xenomai has /proc/xenomai return stat("/proc/xenomai", &sb) == 0; @@ -377,6 +416,7 @@ static int detect_xenomai() { #endif #ifdef USPACE_XENOMAI_EVL static int detect_xenomai_evl() { + if(!has_setuid_root()) return 0; struct stat sb; //Running xenomai evl has /dev/evl but no /proc/xenomai return stat("/dev/evl", &sb) == 0; diff --git a/src/rtapi/uspace_rtapi_main.cc b/src/rtapi/uspace_rtapi_main.cc index 450e625fb91..4049b69c2aa 100644 --- a/src/rtapi/uspace_rtapi_main.cc +++ b/src/rtapi/uspace_rtapi_main.cc @@ -1018,6 +1018,17 @@ static RtapiApp *makeApp() { } else if (detect_rtai()) { app = makeDllApp("liblinuxcnc-uspace-rtai.so.0", SCHED_FIFO); } else { + // SCHED_FIFO available but no Xenomai/RTAI backend. Warn if the + // kernel is not PREEMPT_RT: SCHED_FIFO still beats SCHED_OTHER, + // but latency on a PREEMPT_DYNAMIC stock kernel can be tens of + // milliseconds, which will surprise users who expect the same + // bounds as a PREEMPT_RT or Xenomai setup. + if (!detect_preempt_rt()) { + rtapi_print_msg(RTAPI_MSG_ERR, + "Note: SCHED_FIFO available but kernel is not PREEMPT_RT. " + "Latency may be unbounded; install a PREEMPT_RT kernel " + "for hard realtime guarantees.\n"); + } app = makeDllApp("liblinuxcnc-uspace-posix.so.0", SCHED_FIFO); } } From 4dbe01244992f46068933e7ae0e2e95c3ca41e38 Mon Sep 17 00:00:00 2001 From: Hannes Diethelm Date: Sat, 25 Apr 2026 16:19:14 +0200 Subject: [PATCH 09/17] hm2_eth: Fully cleanup iptables --- src/hal/drivers/mesa-hostmot2/hm2_eth.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/hal/drivers/mesa-hostmot2/hm2_eth.c b/src/hal/drivers/mesa-hostmot2/hm2_eth.c index 914df11cd2d..b3aa6a4ec43 100644 --- a/src/hal/drivers/mesa-hostmot2/hm2_eth.c +++ b/src/hal/drivers/mesa-hostmot2/hm2_eth.c @@ -516,6 +516,12 @@ static void clear_iptables() { shell(IPTABLES" -F "CHAIN" > /dev/null 2>&1"); } +static void cleanup_iptables() { + shell(IPTABLES" -F "CHAIN" > /dev/null 2>&1"); + shell(IPTABLES" -D OUTPUT -j "CHAIN" > /dev/null 2>&1"); + shell(IPTABLES" -X "CHAIN" > /dev/null 2>&1"); +} + static char* inet_ntoa_buf(struct in_addr in, char *buf, size_t n) { const char *addr = inet_ntoa(in); snprintf(buf, n, "%s", addr); @@ -1639,7 +1645,7 @@ void rtapi_app_exit(void) { for(i = 0; i Date: Mon, 27 Apr 2026 15:33:11 +0800 Subject: [PATCH 10/17] rtapi: raise CAP_NET_ADMIN into ambient set, diagnose realtime fallback File capabilities on rtapi_app give cap_net_admin in the permitted and effective sets but not inheritable or ambient, so /sbin/iptables launched by HAL drivers like hm2_eth runs cap-less and fails with EPERM. Raise CAP_NET_ADMIN into the ambient set at startup so it survives execve() into child processes. No-op when the cap is not held. Also expand the fallback-to-POSIX warning to print the sched_setscheduler errno, the effective cap_sys_nice and cap_ipc_lock state via libcap, and mention LINUXCNC_FORCE_REALTIME as a testing-only override. Addresses the diagnostic-output request in issue #3928. Switch the existing detect_preempt_rt and has_setuid_root helpers from __attribute__((unused)) to static inline for codebase consistency. --- src/Makefile | 3 ++ src/rtapi/Submakefile | 2 +- src/rtapi/uspace_common.h | 36 ++++++++++------- src/rtapi/uspace_rtapi_main.cc | 74 ++++++++++++++++++++++++++++++++-- 4 files changed, 96 insertions(+), 19 deletions(-) diff --git a/src/Makefile b/src/Makefile index 45354ba4349..94fdadc607e 100644 --- a/src/Makefile +++ b/src/Makefile @@ -580,6 +580,9 @@ endif # cap_net_admin - raw socket access for hm2_eth / iptables management # cap_sys_rawio - iopl() and /dev/mem for parallel port and PCI I/O # cap_sys_nice - SCHED_FIFO scheduling and CPU affinity +# Linux capabilities are not inherited across exec(), so /sbin/iptables +# launched from rtapi_app would run unprivileged. rtapi_app raises +# cap_net_admin into its ambient set at startup so it survives execve(). # Clears any setuid bit left by a prior 'make setuid' so the two paths don't # silently stack. setcap: diff --git a/src/rtapi/Submakefile b/src/rtapi/Submakefile index 67920ba725e..0ca738d01ab 100644 --- a/src/rtapi/Submakefile +++ b/src/rtapi/Submakefile @@ -44,7 +44,7 @@ $(call TOOBJSDEPS, $(RTAPI_APP_SRCS)): EXTRAFLAGS += -DSIM \ -UULAPI -DRTAPI -pthread ../bin/rtapi_app: $(call TOOBJS, $(RTAPI_APP_SRCS)) $(ECHO) Linking $(notdir $@) - $(Q)$(CXX) -rdynamic -o $@ $^ $(LIBDL) -pthread -lrt -lfmt $(LIBUDEV_LIBS) -ldl $(LDFLAGS) + $(Q)$(CXX) -rdynamic -o $@ $^ $(LIBDL) -pthread -lrt -lfmt $(LIBUDEV_LIBS) -ldl -lcap $(LDFLAGS) TARGETS += ../bin/rtapi_app USPACE_POSIX_SRCS := rtapi/uspace_posix.cc diff --git a/src/rtapi/uspace_common.h b/src/rtapi/uspace_common.h index 8ee75734841..a7981164b37 100644 --- a/src/rtapi/uspace_common.h +++ b/src/rtapi/uspace_common.h @@ -359,18 +359,14 @@ int rtapi_is_kernelspace() { return 0; } // for diagnostic warning at startup; callers must not gate behavior on // the kernel string, since SCHED_FIFO on a PREEMPT_DYNAMIC kernel is still // useful (better than SCHED_OTHER, worse than PREEMPT_RT). -// Marked unused because uspace_common.h is also included from ULAPI TUs -// that do not reference it. -__attribute__((unused)) -static int detect_preempt_rt() { +static inline int detect_preempt_rt() { struct utsname u; if(uname(&u) < 0) return 0; return strcasestr(u.version, "PREEMPT RT") != 0 || strcasestr(u.version, "PREEMPT_RT") != 0; } #else -__attribute__((unused)) -static int detect_preempt_rt() { +static inline int detect_preempt_rt() { return 0; } #endif @@ -382,11 +378,7 @@ static int detect_preempt_rt() { // with udev rules + a 'xenomai'/'evl' group; @hdiethelm has a follow-up // planned. Until then, an unprivileged user on a Xenomai kernel cannot // claim the Xenomai backend, and falls back to the SCHED_FIFO probe. -// Marked unused because the helper is called only when one of the -// USPACE_RTAI / USPACE_XENOMAI / USPACE_XENOMAI_EVL macros is defined, -// and the header is included from ULAPI TUs that define none of them. -__attribute__((unused)) -static int has_setuid_root() { +static inline int has_setuid_root() { return geteuid() == 0; } @@ -427,6 +419,11 @@ static int detect_xenomai_evl() { } #endif +// errno from the most recent sched_setscheduler(SCHED_FIFO) probe. Zero +// when the probe succeeded or has not run yet. Read via +// rtapi_sched_fifo_errno() from diagnostic code. +static int rtapi_sched_fifo_last_errno = 0; + // Success-probe for realtime scheduling: briefly try to set SCHED_FIFO on // the calling thread and restore the previous policy. Succeeds when the // process holds CAP_SYS_NICE (file caps or setuid root) or has a matching @@ -436,20 +433,31 @@ static int detect_xenomai_evl() { static int can_set_sched_fifo(void) { struct sched_param old_param, probe_param; int old_policy = sched_getscheduler(0); - if(old_policy < 0) return 0; - if(sched_getparam(0, &old_param) < 0) return 0; + if(old_policy < 0) { + rtapi_sched_fifo_last_errno = errno; + return 0; + } + if(sched_getparam(0, &old_param) < 0) { + rtapi_sched_fifo_last_errno = errno; + return 0; + } memset(&probe_param, 0, sizeof(probe_param)); probe_param.sched_priority = sched_get_priority_min(SCHED_FIFO); - if(sched_setscheduler(0, SCHED_FIFO, &probe_param) < 0) + if(sched_setscheduler(0, SCHED_FIFO, &probe_param) < 0) { + rtapi_sched_fifo_last_errno = errno; return 0; + } // Best-effort restore; if this fails we are still on SCHED_FIFO at // minimum priority, which is no worse than where we started. sched_setscheduler(0, old_policy, &old_param); + rtapi_sched_fifo_last_errno = 0; return 1; } +static inline int rtapi_sched_fifo_errno(void) { return rtapi_sched_fifo_last_errno; } + // rtapi_is_realtime() reports whether this process can actually run // realtime code. This matches the convention used by JACK, PipeWire, // rtkit, Xenomai, and Klipper: surface the observed capability, not diff --git a/src/rtapi/uspace_rtapi_main.cc b/src/rtapi/uspace_rtapi_main.cc index 4049b69c2aa..5e4e438ca30 100644 --- a/src/rtapi/uspace_rtapi_main.cc +++ b/src/rtapi/uspace_rtapi_main.cc @@ -46,6 +46,7 @@ #ifdef __linux__ #include #include +#include #endif #ifdef __FreeBSD__ #include @@ -689,6 +690,10 @@ static double diff_timespec(const struct timespec *time1, const struct timespec return (double)(time1->tv_sec - time0->tv_sec) + (double)(time1->tv_nsec - time0->tv_nsec) / 1000000000.0; } +#ifdef __linux__ +static void raise_net_admin_ambient(void); +#endif + int main(int argc, char **argv) { if (getuid() == 0) { char *fallback_uid_str = getenv("RTAPI_UID"); @@ -720,6 +725,7 @@ int main(int argc, char **argv) { } #ifdef __linux__ setfsuid(ruid); + raise_net_admin_ambient(); #endif std::vector args; for (int i = 1; i < argc; i++) { @@ -997,15 +1003,75 @@ static RtapiApp *makeDllApp(const std::string &dllName, int policy) { return result; } +// Diagnostic helper: report cap_effective state for a single capability. +// Returns "yes", "no", or "unknown" if libcap could not introspect. +#ifdef __linux__ +static const char *cap_effective_str(cap_t caps, cap_value_t cap) { + if (!caps) return "unknown"; + cap_flag_value_t v; + if (cap_get_flag(caps, cap, CAP_EFFECTIVE, &v) != 0) return "unknown"; + return v == CAP_SET ? "yes" : "no"; +} + +// Raise CAP_NET_ADMIN into the ambient set so it survives execve() into +// child processes (iptables, ip6tables) launched by HAL drivers like +// hm2_eth. Linux file capabilities on rtapi_app give cap_net_admin in +// the permitted+effective sets but not inheritable/ambient, so without +// this iptables runs cap-less and fails with EPERM. No-op when the cap +// is not held (e.g. running unprivileged). +static void raise_net_admin_ambient(void) { + cap_t caps = cap_get_proc(); + if (!caps) return; + + cap_value_t cap = CAP_NET_ADMIN; + cap_flag_value_t v; + if (cap_get_flag(caps, cap, CAP_PERMITTED, &v) == 0 && v == CAP_SET) { + if (cap_set_flag(caps, CAP_INHERITABLE, 1, &cap, CAP_SET) == 0 + && cap_set_proc(caps) == 0) { + if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, + CAP_NET_ADMIN, 0, 0) != 0 + && geteuid() != 0) { + rtapi_print_msg(RTAPI_MSG_WARN, + "rtapi_app: PR_CAP_AMBIENT_RAISE(CAP_NET_ADMIN) " + "failed: %s; iptables-using drivers may not work " + "under file caps.\n", strerror(errno)); + } + } + } + cap_free(caps); +} +#endif + static RtapiApp *makeApp() { RtapiApp *app; bool rt_ok = rtapi_is_realtime(); if (!rt_ok) { + // Surface the actual reason so the user does not have to guess + // between "no caps", "stock kernel", or "wrong rlimits" (issue + // #3928). errno comes from the SCHED_FIFO probe in + // can_set_sched_fifo(); cap state comes from libcap. + int sched_err = rtapi_sched_fifo_errno(); +#ifdef __linux__ + cap_t caps = cap_get_proc(); + const char *nice_s = cap_effective_str(caps, CAP_SYS_NICE); + const char *lock_s = cap_effective_str(caps, CAP_IPC_LOCK); +#else + const char *nice_s = "unknown"; + const char *lock_s = "unknown"; +#endif rtapi_print_msg(RTAPI_MSG_ERR, - "Note: SCHED_FIFO not permitted for this process, " - "falling back to POSIX non-realtime. " - "Run 'sudo make setcap' (preferred) or 'sudo make setuid' " - "on rtapi_app to enable realtime scheduling.\n"); + "Note: realtime scheduling unavailable " + "(sched_setscheduler SCHED_FIFO: %s).\n" + " Process capabilities: cap_sys_nice=%s cap_ipc_lock=%s.\n" + " Falling back to POSIX non-realtime.\n" + " Fix: 'sudo make setcap' (preferred) or 'sudo make setuid' " + "on rtapi_app.\n" + " Override (testing only): set LINUXCNC_FORCE_REALTIME=1.\n", + sched_err ? strerror(sched_err) : "denied", + nice_s, lock_s); +#ifdef __linux__ + if (caps) cap_free(caps); +#endif } if (!rt_ok || harden_rt() < 0) { app = makeDllApp("liblinuxcnc-uspace-posix.so.0", SCHED_OTHER); From f8ab7c3fa5d7dde9124f5bccc73c2fc66a15a2b7 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:33:22 +0800 Subject: [PATCH 11/17] hm2_eth: install iptables and ip6tables rules directly Replace the shell-based "iptables" macro path with structured posix_spawn calls into /sbin/iptables and /sbin/ip6tables. The rtapi_app process raises CAP_NET_ADMIN into its ambient set at startup, so the calls succeed under both setuid-root and rootless (file-cap) installs without a separate helper binary. Restore --sport in the per-board ACCEPT rule: cleanup_iptables() now removes the chain on exit, so a stale rule from a previous run with a different ephemeral port cannot block the second invocation. Replace the IPv6 sysctl call (which needs CAP_DAC_OVERRIDE) with an ip6tables OUTPUT DROP on a parallel hm2-eth-rules-output chain, so the firewall stays inside the cap_net_admin envelope. Update the manual recipe in hm2_eth(9) to match, and document the optional sysctl.conf step for users who want full IPv6 quiescence. --- docs/src/man/man9/hm2_eth.9.adoc | 64 ++++++- src/hal/drivers/mesa-hostmot2/hm2_eth.c | 229 ++++++++++++++---------- 2 files changed, 186 insertions(+), 107 deletions(-) diff --git a/docs/src/man/man9/hm2_eth.9.adoc b/docs/src/man/man9/hm2_eth.9.adoc index 8627786a93c..cdb29cedddd 100644 --- a/docs/src/man/man9/hm2_eth.9.adoc +++ b/docs/src/man/man9/hm2_eth.9.adoc @@ -17,15 +17,14 @@ ____ As shipped, the board address is 192.168.1.121. *no_iptables* [default: 0]:: Explicit override that disables all iptables interaction. By default - hm2_eth probes iptables at load time with a read-only listing and - silently skips rule installation if the probe fails (for example - when rtapi_app is running unprivileged via file capabilities rather - than setuid root, since Linux capabilities are not inherited across - exec() into the iptables binary). Set *no_iptables=1* when iptables - is reachable but you prefer to manage the firewall externally - (nftables, firewalld, systemd units). In both cases the - interface-isolation rules must be provided by the administrator; see - the NOTES section below. + hm2_eth installs *iptables* and *ip6tables* rules itself; rtapi_app + raises *cap_net_admin* into its ambient capability set at startup so + the calls succeed under both setuid-root and rootless (file-cap) + installs. If the cap is not held the probe fails and rule + installation is skipped with a warning; in that case configure the + rules manually using the recipe in the NOTES section below. Set + *no_iptables=1* when iptables is reachable but you prefer to manage + the firewall externally (nftables, firewalld, systemd units). ____ == DESCRIPTION @@ -157,6 +156,53 @@ At (normal) exit, hm2_eth will remove the rules. After a crash, you can manually clear the rules with *sudo iptables -F hm2-eth-rules-output*; the rules are also removed by a reboot. +=== Manual iptables configuration + +When LinuxCNC is installed without *cap_net_admin* on rtapi_app +(typically because *sudo make setcap* was not run after the build), +hm2_eth cannot install its rules and prints a warning. Set up the +chain manually as root. Adjust the IP addresses, UDP destination port, +and interface name to match your install: + +---- +HOST_IP=192.168.1.1 +BOARD_IP=192.168.1.121 +BOARD_DPORT=27181 +IFACE=eth1 + +iptables -N hm2-eth-rules-output +iptables -I OUTPUT 1 -j hm2-eth-rules-output +iptables -A hm2-eth-rules-output \ + -p udp -m udp -d $BOARD_IP --dport $BOARD_DPORT \ + -s $HOST_IP -j ACCEPT +iptables -A hm2-eth-rules-output -o $IFACE -p icmp -j DROP +iptables -A hm2-eth-rules-output -o $IFACE \ + -j REJECT --reject-with icmp-admin-prohibited +ip6tables -N hm2-eth-rules-output +ip6tables -I OUTPUT 1 -j hm2-eth-rules-output +ip6tables -A hm2-eth-rules-output -o $IFACE -j DROP +---- + +For full IPv6 quiescence (no router solicitations or neighbor discovery +on the dedicated interface), additionally add this line to +`/etc/sysctl.d/99-hm2-eth.conf` and reboot: + +---- +net.ipv6.conf.IFACE.disable_ipv6 = 1 +---- + +(The default ip6tables rule above only drops outbound IPv6; the kernel +still generates the packets.) Tear down the runtime rules with: + +---- +iptables -F hm2-eth-rules-output +iptables -D OUTPUT -j hm2-eth-rules-output +iptables -X hm2-eth-rules-output +ip6tables -F hm2-eth-rules-output +ip6tables -D OUTPUT -j hm2-eth-rules-output +ip6tables -X hm2-eth-rules-output +---- + "hardware-irq-coalesce-rx-usecs" decreases time waiting to receive a packet on most systems, but on at least some Marvel-chipset NICs it is harmful. If the line does not improve system performance, then remove it. diff --git a/src/hal/drivers/mesa-hostmot2/hm2_eth.c b/src/hal/drivers/mesa-hostmot2/hm2_eth.c index b3aa6a4ec43..5d78d85cf7e 100644 --- a/src/hal/drivers/mesa-hostmot2/hm2_eth.c +++ b/src/hal/drivers/mesa-hostmot2/hm2_eth.c @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -45,7 +46,6 @@ #include "hostmot2-lowlevel.h" #include "hostmot2.h" #include "hm2_eth.h" -#include "eshellf.h" struct kvlist { struct rtapi_list_head list; @@ -466,60 +466,115 @@ static hm2_eth_t boards[MAX_ETH_BOARDS]; static int eth_socket_send(int sockfd, const void *buffer, int len, int flags); static int eth_socket_recv(int sockfd, void *buffer, int len, int flags); -#define IPTABLES "env \"PATH=/usr/sbin:/sbin:${PATH}\" iptables" +// hm2_eth installs iptables/ip6tables rules to isolate the dedicated Mesa +// interface from non-realtime traffic. rtapi_app raises CAP_NET_ADMIN +// into its ambient set at startup (see uspace_rtapi_main.cc), so the +// caps survive execve() into the iptables binaries even when we run +// under file caps instead of setuid root. +#define IPTABLES_BIN "/sbin/iptables" +#define IP6TABLES_BIN "/sbin/ip6tables" #define CHAIN "hm2-eth-rules-output" -static bool chain_exists() { - int result = - shell(IPTABLES" -n -L "CHAIN" > /dev/null 2>&1"); - return result == EXIT_SUCCESS; +// run_iptables(): fork+exec the named iptables binary with a +// NULL-terminated argv list (sentinel == NULL, do not omit). Returns +// the child exit status, or -1 on spawn/wait failure. When quiet, the +// child's stdout+stderr are suppressed so probe-style "is this rule +// present?" calls do not spam the log on the (expected) failure path. +static int run_iptables(const char *bin, int quiet, ...) { + char *argv[24]; + int n = 0; + // argv[0] is the program name iptables expects, not the path. + argv[n++] = (char *)(strrchr(bin, '/') ? strrchr(bin, '/') + 1 : bin); + + va_list ap; + va_start(ap, quiet); + while(n < (int)(sizeof(argv)/sizeof(argv[0])) - 1) { + char *a = va_arg(ap, char *); + if(!a) break; + argv[n++] = a; + } + argv[n] = NULL; + va_end(ap); + + posix_spawn_file_actions_t fa, *pfa = NULL; + if(quiet && posix_spawn_file_actions_init(&fa) == 0) { + if(posix_spawn_file_actions_addopen(&fa, STDOUT_FILENO, + "/dev/null", O_WRONLY, 0) == 0 + && posix_spawn_file_actions_adddup2(&fa, STDOUT_FILENO, + STDERR_FILENO) == 0) { + pfa = &fa; + } + } + + pid_t pid; + int r = posix_spawn(&pid, bin, pfa, NULL, argv, environ); + if(pfa) posix_spawn_file_actions_destroy(&fa); + + if(r != 0) return -1; + int status; + if(waitpid(pid, &status, 0) < 0) return -1; + if(WIFEXITED(status)) return WEXITSTATUS(status); + return -1; } +#define IPT(quiet, ...) run_iptables(IPTABLES_BIN, (quiet), __VA_ARGS__, NULL) +#define IP6T(quiet, ...) run_iptables(IP6TABLES_BIN, (quiet), __VA_ARGS__, NULL) + static int iptables_state = -1; static bool use_iptables() { if(iptables_state == -1) { if(no_iptables) { - LL_PRINT("Skipping iptables setup (no_iptables=1); configure firewall externally.\n"); + LL_PRINT("Skipping iptables setup (no_iptables=1); " + "configure firewall externally.\n"); return (iptables_state = 0); } - // Pre-flight probe: capabilities held by rtapi_app do not propagate - // across exec(), so when we run unprivileged (file-caps instead of - // setuid root) iptables cannot touch the kernel tables. A read-only - // list of a built-in chain is the cheapest way to find out without - // leaving side effects. - if(shell(IPTABLES " -n -L INPUT > /dev/null 2>&1") != EXIT_SUCCESS) { - LL_PRINT("iptables is not available to this process " - "(running unprivileged?); skipping automatic rule " - "installation. Configure firewall externally to " - "isolate the hm2-eth interface.\n"); + // Read-only probe: list the INPUT chain. Fails when the + // process lacks CAP_NET_ADMIN (rootless without setcap, or + // setcap applied but ambient raise failed in rtapi_app). + if(IPT(1, "-n", "-L", "INPUT") != 0) { + LL_PRINT("iptables not available (missing CAP_NET_ADMIN?); " + "automatic firewall setup skipped. See hm2_eth(9) " + "NOTES for the manual rule recipe.\n"); return (iptables_state = 0); } - if(!chain_exists()) { - int res = shell(IPTABLES " -N " CHAIN); - if(res != EXIT_SUCCESS) { - LL_PRINT("ERROR: Failed to create iptables chain "CHAIN); + // Create chain only if absent; insert OUTPUT jump only if absent. + if(IPT(1, "-n", "-L", CHAIN) != 0) { + if(IPT(0, "-N", CHAIN) != 0) { + LL_PRINT("ERROR: failed to create iptables chain " CHAIN "\n"); return (iptables_state = 0); } } - // now add a jump to our chain at the start of the OUTPUT chain if it isn't in the chain already - int res = shell(IPTABLES "-C OUTPUT -j " CHAIN " 2>/dev/null || /sbin/iptables -I OUTPUT 1 -j " CHAIN); - if(res != EXIT_SUCCESS) { - LL_PRINT("ERROR: Failed to insert rule in OUTPUT chain"); - return (iptables_state = 0); + if(IPT(1, "-C", "OUTPUT", "-j", CHAIN) != 0) { + if(IPT(0, "-I", "OUTPUT", "1", "-j", CHAIN) != 0) { + LL_PRINT("ERROR: failed to insert OUTPUT jump\n"); + return (iptables_state = 0); + } } + // Mirror the chain for ip6tables so IPv6 isolation can hang off + // it. Best-effort: kernels without IPv6 support cause this to + // fail silently and the IPv6 rules are simply absent. + if(IP6T(1, "-n", "-L", CHAIN) != 0) + IP6T(1, "-N", CHAIN); + if(IP6T(1, "-C", "OUTPUT", "-j", CHAIN) != 0) + IP6T(1, "-I", "OUTPUT", "1", "-j", CHAIN); + return (iptables_state = 1); } return iptables_state; } static void clear_iptables() { - shell(IPTABLES" -F "CHAIN" > /dev/null 2>&1"); + IPT(1, "-F", CHAIN); + IP6T(1, "-F", CHAIN); } static void cleanup_iptables() { - shell(IPTABLES" -F "CHAIN" > /dev/null 2>&1"); - shell(IPTABLES" -D OUTPUT -j "CHAIN" > /dev/null 2>&1"); - shell(IPTABLES" -X "CHAIN" > /dev/null 2>&1"); + IPT(1, "-F", CHAIN); + IPT(1, "-D", "OUTPUT", "-j", CHAIN); + IPT(1, "-X", CHAIN); + IP6T(1, "-F", CHAIN); + IP6T(1, "-D", "OUTPUT", "-j", CHAIN); + IP6T(1, "-X", CHAIN); } static char* inet_ntoa_buf(struct in_addr in, char *buf, size_t n) { @@ -555,46 +610,10 @@ static char* fetch_ifname(int sockfd, char *buf, size_t n) { return NULL; } -static char *vseprintf(char *buf, char *ebuf, const char *fmt, va_list ap) { - int result = vsnprintf(buf, ebuf-buf, fmt, ap); - if(result < 0) return ebuf; - else if(buf + result > ebuf) return ebuf; - else return buf + result; -} - -static char *seprintf(char *buf, char *ebuf, const char *fmt, ...) { - va_list ap; - va_start(ap, fmt); - char *result = vseprintf(buf, ebuf, fmt, ap); - va_end(ap); - return result; -} - -static int install_iptables_rule(const char *fmt, ...) { - char commandbuf[1024], *ptr = commandbuf, - *ebuf = commandbuf + sizeof(commandbuf); - ptr = seprintf(ptr, ebuf, IPTABLES" -A "CHAIN" "); - va_list ap; - va_start(ap, fmt); - ptr = vseprintf(ptr, ebuf, fmt, ap); - va_end(ap); - - if(ptr == ebuf) - { - LL_PRINT("ERROR: commandbuf too small\n"); - return -ENOSPC; - } - - int res = shell(commandbuf); - if(res == EXIT_SUCCESS) return 0; - - LL_PRINT("ERROR: Failed to execute '%s'\n", commandbuf); - return -EINVAL; -} - static int install_iptables_board(int sockfd) { struct sockaddr_in srcaddr, dstaddr; char srchost[16], dsthost[16]; // enough for 255.255.255.255\0 + char dport_s[8], sport_s[8]; socklen_t addrlen = sizeof(srcaddr); int res = getsockname(sockfd, &srcaddr, &addrlen); @@ -604,33 +623,48 @@ static int install_iptables_board(int sockfd) { res = getpeername(sockfd, &dstaddr, &addrlen); if(res < 0) return -errno; - res = install_iptables_rule( - "-p udp -m udp -d %s --dport %d -s %s --sport %d -j ACCEPT", - inet_ntoa_buf(dstaddr.sin_addr, dsthost, sizeof(dsthost)), - ntohs(dstaddr.sin_port), - inet_ntoa_buf(srcaddr.sin_addr, srchost, sizeof(srchost)), - ntohs(srcaddr.sin_port)); - return res; + if(!use_iptables()) return 0; + + inet_ntoa_buf(srcaddr.sin_addr, srchost, sizeof(srchost)); + inet_ntoa_buf(dstaddr.sin_addr, dsthost, sizeof(dsthost)); + snprintf(dport_s, sizeof(dport_s), "%d", ntohs(dstaddr.sin_port)); + snprintf(sport_s, sizeof(sport_s), "%d", ntohs(srcaddr.sin_port)); + + // --sport is safe here: cleanup_iptables() removes the chain on exit, + // so a stale rule from a previous run with a different ephemeral port + // cannot block the second invocation. + if(IPT(0, "-A", CHAIN, + "-p", "udp", "-m", "udp", + "-d", dsthost, "--dport", dport_s, + "-s", srchost, "--sport", sport_s, + "-j", "ACCEPT") != 0) + return -EINVAL; + return 0; } static int install_iptables_perinterface(const char *ifbuf) { - // without this rule, 'ping' spews a lot of messages like - // From 192.168.1.1 icmp_seq=5 Packet filtered - // many times for each ping packet sent. With this rule, - // ping prints 'ping: sendmsg: Operation not permitted' once - // per second. - int res = install_iptables_rule( - "-o %s -p icmp -j DROP", - ifbuf); - if(res < 0) return res; - - res = install_iptables_rule( - "-o %s -j REJECT --reject-with icmp-admin-prohibited", - ifbuf); - if(res < 0) return res; - - res = eshellf(HM2_LLIO_NAME, "/sbin/sysctl -q net.ipv6.conf.%s.disable_ipv6=1", ifbuf); - if(res < 0) return res; + // Without these rules, 'ping' spews a lot of "Packet filtered" + // messages. With them, ping prints 'ping: sendmsg: Operation not + // permitted' once per second. + // + // Outbound IPv6 on the dedicated interface is dropped via ip6tables + // rather than disable_ipv6 sysctl: writing the sysctl needs + // CAP_DAC_OVERRIDE (file is mode 644 root:root) and we'd rather not + // grant it to rtapi_app. Users who want full IPv6 quiescence (no + // router solicitations etc.) can additionally set + // 'net.ipv6.conf..disable_ipv6=1' in /etc/sysctl.conf. + if(!use_iptables()) return 0; + + if(IPT(0, "-A", CHAIN, "-o", (char *)ifbuf, "-p", "icmp", "-j", "DROP") != 0) + return -EINVAL; + if(IPT(0, "-A", CHAIN, "-o", (char *)ifbuf, + "-j", "REJECT", "--reject-with", "icmp-admin-prohibited") != 0) + return -EINVAL; + + // ip6tables is best-effort: kernel may not have IPv6 support + // compiled in, in which case the chain creation in use_iptables() + // already failed and this rule is simply absent. + IP6T(1, "-A", CHAIN, "-o", (char *)ifbuf, "-j", "DROP"); return 0; } @@ -734,11 +768,11 @@ static int init_board(hm2_eth_t *board, const char *board_ip) { return -errno; } - if(use_iptables()) - { - ret = install_iptables_board(board->sockfd); - if(ret < 0) return ret; - } + // install_iptables_board() is a no-op when iptables is unavailable + // (rootless install without setcap on hm2_eth_iptables, or + // no_iptables=1), so it is safe to call unconditionally. + ret = install_iptables_board(board->sockfd); + if(ret < 0) return ret; board->write_packet_ptr = board->write_packet; board->read_packet_ptr = board->read_packet; @@ -1620,8 +1654,7 @@ int rtapi_app_main(void) { if(!added) goto error; if(*added) continue; - if(use_iptables()) - install_iptables_perinterface(ifptr); + install_iptables_perinterface(ifptr); *added = 1; } From 1da9b9081c3d054e3bf566e59f9cc4322ac89176 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:33:26 +0800 Subject: [PATCH 12/17] debian: add libcap-dev build dep rtapi_app links libcap to introspect process capabilities for the realtime-fallback diagnostic and to raise CAP_NET_ADMIN into its ambient set at startup. --- debian/control.top.in | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control.top.in b/debian/control.top.in index 1016d940fc9..54317ce6102 100644 --- a/debian/control.top.in +++ b/debian/control.top.in @@ -28,6 +28,7 @@ Build-Depends: libgl-dev | libgl1-mesa-dev, libglu1-mesa-dev, libgtk-3-dev, + libcap-dev, libmodbus-dev (>= 3.0), libgpiod-dev, @LIBREADLINE_DEV@, From a3413f091fe29508ec70ef7640908b5e9555efbb Mon Sep 17 00:00:00 2001 From: Damian Wrobel Date: Mon, 25 May 2020 20:31:29 +0200 Subject: [PATCH 13/17] rootless: incr_mem_usage() Allow the incr_mem_usage() to be used without root privileges. Signed-off-by: Damian Wrobel --- src/hal/utils/upci.c | 74 +++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/src/hal/utils/upci.c b/src/hal/utils/upci.c index 0a18f7b87b2..460ca4e25b7 100644 --- a/src/hal/utils/upci.c +++ b/src/hal/utils/upci.c @@ -401,37 +401,49 @@ static void decr_io_usage ( void ) static int incr_mem_usage ( void ) { - int eno; - - /* make sure /dev/mem is open */ - if ( memaccess == 0 ) { - /* open it */ - /* this needs privileges */ - if (seteuid(0) != 0) { - errmsg(__func__, "need root privileges (or setuid root)"); - return -1; - } - /* do it */ - memfd = open("/dev/mem", O_RDWR); - eno = errno; - /* drop privileges */ - if(seteuid(getuid()) != 0) - { - errmsg(__func__, "unable to drop root privileges"); - /* Don't continue past this point, because following code may - * execute with unexpected privileges - */ - _exit(99); - } - /* check result */ - if ( memfd < 0 ) { - errmsg(__func__,"can't open /dev/mem: %s", strerror(eno)); - return -1; - } - } - /* increment reference count */ - memaccess++; - return 0; + int retval = 0, eno = 0; + + /* Try to open /dev/mem with our existing privileges first: succeeds + * when the process holds CAP_SYS_RAWIO via file caps or is already + * setuid root. Only fall back to seteuid(0) if that path is closed. + */ + do { + if (memaccess) { + break; + } + + memfd = open("/dev/mem", O_RDWR); + if (memfd >= 0) { + break; + } + + retval = seteuid(0); + if (retval != 0) { + eno = errno; + break; + } + + memfd = open("/dev/mem", O_RDWR); + retval = memfd >= 0 ? 0 : memfd; + eno = errno; + + if (seteuid(getuid()) != 0) { + errmsg(__func__, "unable to drop root privileges"); + /* Don't continue past this point, because following code may + * execute with unexpected privileges + */ + _exit(99); + } + } while (0); + + if (retval == 0) { + /* increment reference count */ + memaccess++; + } else { + errmsg(__func__,"error: %s", strerror(eno)); + } + + return retval; } static void decr_mem_usage ( void ) From b8724025ea515286d576bcb7e4a2cf31eb147c94 Mon Sep 17 00:00:00 2001 From: Damian Wrobel Date: Mon, 25 May 2020 20:09:13 +0200 Subject: [PATCH 14/17] rootless: incr_io_usage() Allow the incr_io_usage() to be used without root privileges. Signed-off-by: Damian Wrobel --- src/hal/utils/upci.c | 71 +++++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/src/hal/utils/upci.c b/src/hal/utils/upci.c index 460ca4e25b7..89d79855293 100644 --- a/src/hal/utils/upci.c +++ b/src/hal/utils/upci.c @@ -352,37 +352,48 @@ int upci_find_device(struct upci_dev_info *p) static int incr_io_usage ( void ) { - int retval, eno; - - /* make sure we can do I/O */ - if ( ioaccess == 0 ) { - /* enable access */ - /* this needs privileges */ - if (seteuid(0) != 0) { - errmsg(__func__, "need root privileges (or setuid root)"); - return -1; - } - /* do it */ - retval = iopl(3); - eno = errno; - /* drop privileges */ - if(seteuid(getuid()) != 0) - { - errmsg(__func__, "unable to drop root privileges"); - /* Don't continue past this point, because following code may - * execute with unexpected privileges - */ - _exit(99); - } - /* check result */ - if(retval < 0 || iopl(3) < 0) { - errmsg(__func__,"opening I/O ports: %s", strerror(eno)); - return -1; - } + int retval = 0, eno = 0; + + /* Try iopl(3) with our existing privileges first: succeeds when + * the process holds CAP_SYS_RAWIO via file caps or is already + * setuid root. Only fall back to seteuid(0) if that path is closed. + */ + do { + if (ioaccess) { + break; + } + + retval = iopl(3); + if (retval == 0) { + break; + } + + retval = seteuid(0); + if (retval != 0) { + eno = errno; + break; + } + + retval = iopl(3); + eno = errno; + + if (seteuid(getuid()) != 0) { + errmsg(__func__, "unable to drop root privileges"); + /* Don't continue past this point, because following code may + * execute with unexpected privileges + */ + _exit(99); + } + } while (0); + + if (retval == 0) { + /* increment reference count */ + ioaccess++; + } else { + errmsg(__func__,"error: %s", strerror(eno)); } - /* increment reference count */ - ioaccess++; - return 0; + + return retval; } static void decr_io_usage ( void ) From 937427cb32451932012e085142b0d94348174675 Mon Sep 17 00:00:00 2001 From: Damian Wrobel Date: Mon, 25 May 2020 21:11:31 +0200 Subject: [PATCH 15/17] Remove direct usage of inb(), outb() Signed-off-by: Damian Wrobel --- src/hal/drivers/hal_evoreg.c | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/hal/drivers/hal_evoreg.c b/src/hal/drivers/hal_evoreg.c index 5bc2ee34287..cf33a78a11f 100644 --- a/src/hal/drivers/hal_evoreg.c +++ b/src/hal/drivers/hal_evoreg.c @@ -66,20 +66,9 @@ #include /* isspace() */ #include /* RTAPI realtime OS API */ #include /* RTAPI realtime module decls */ +#include /* rtapi_inb(), rtapi_outb() */ #include /* HAL public API decls */ -/* If FASTIO is defined, uses outb() and inb() from , - instead of rtapi_outb() and rtapi_inb() - the ones - are inlined, and save a microsecond or two (on my 233MHz box) -*/ -#define FASTIO - -#ifdef FASTIO -#define rtapi_inb inb -#define rtapi_outb outb -#include -#endif - /* module information */ MODULE_AUTHOR("Martin Kuhnle"); MODULE_DESCRIPTION("SIEMENS-EVOREG Driver for EMC HAL"); From 875e5f7ebc8d4d166e909d1c90607462dabeb5f3 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Fri, 15 May 2026 20:52:31 +0800 Subject: [PATCH 16/17] hm2_rpspi/hm2_spix: detect kernel SPI driver, fail at load Replace the eshellf-driven modprobe/rmmod dance with a passive check: on load, look for the conflicting kernel SPI master driver (spi_bcm2835 on RPi3/4, dw_spi_mmio on RPi5) and refuse to start if present, with a message pointing at raspi-config or the modprobe.d blacklist recipe in the man page. Avoids needing root to call /sbin/rmmod and removes the last user of rtapi_spawn_as_root from these drivers. The shared kernel_module_loaded() helper queries /sys/module/, which covers both loadable and built-in modules; /proc/modules alone would miss kernels with SPI compiled in. Lives in a new kmod_check.c linked into hm2_rpspi and hm2_spix, replacing eshellf.c which has no remaining callers (hm2_eth no longer needs it either after the direct iptables posix_spawn rework). Update hm2_rpspi(9) and hm2_spix(9) NOTES to describe the per-platform disable recipe instead of claiming auto-unload. --- docs/src/man/man9/hm2_rpspi.9.adoc | 12 ++-- docs/src/man/man9/hm2_spix.9.adoc | 28 ++++++--- src/Makefile | 5 +- src/hal/drivers/mesa-hostmot2/eshellf.c | 63 ------------------- src/hal/drivers/mesa-hostmot2/hm2_rpspi.c | 11 +++- .../mesa-hostmot2/{eshellf.h => kmod_check.c} | 23 +++---- src/hal/drivers/mesa-hostmot2/kmod_check.h | 40 ++++++++++++ src/hal/drivers/mesa-hostmot2/spix_rpi3.c | 24 ++++--- src/hal/drivers/mesa-hostmot2/spix_rpi5.c | 24 ++++--- 9 files changed, 113 insertions(+), 117 deletions(-) delete mode 100644 src/hal/drivers/mesa-hostmot2/eshellf.c rename src/hal/drivers/mesa-hostmot2/{eshellf.h => kmod_check.c} (64%) create mode 100644 src/hal/drivers/mesa-hostmot2/kmod_check.h diff --git a/docs/src/man/man9/hm2_rpspi.9.adoc b/docs/src/man/man9/hm2_rpspi.9.adoc index 91938e2aea4..82280d76777 100644 --- a/docs/src/man/man9/hm2_rpspi.9.adoc +++ b/docs/src/man/man9/hm2_rpspi.9.adoc @@ -70,10 +70,14 @@ Mesa's SPI based Anything I/O boards (with the HostMot2 firmware) to the LinuxCNC HAL. This driver is not based on the linux spidev driver, but on a dedicated BCM2835-SPI driver. -It is *strongly* recommended that you unload/disable the kernel's spidev -driver by disabling it using *raspi-config*. Please note that having -both kernel and user-space SPI drivers installed can result in -unexpected interactions and system instabilities. +The kernel's *spi_bcm2835* driver conflicts with this user-space driver +and must be disabled before *hm2_rpspi* will load. Use *raspi-config* +(Interface Options -> SPI -> Disable) and reboot. If the kernel module +is still present at load time the driver will refuse to start with +"Kernel SPI driver spi_bcm2835 is loaded and conflicts" rather than +fight the kernel for the bus. Having both kernel and user-space SPI +drivers installed otherwise leads to unexpected interactions and +system instabilities. The supported boards are: 7I90HD. diff --git a/docs/src/man/man9/hm2_spix.9.adoc b/docs/src/man/man9/hm2_spix.9.adoc index 47cc2e82ac1..2cf92b6c89c 100644 --- a/docs/src/man/man9/hm2_spix.9.adoc +++ b/docs/src/man/man9/hm2_spix.9.adoc @@ -208,13 +208,27 @@ setting would be to set one step below the maximum speeds. == NOTES -If you know your setup and do not require the spix_spidev driver, then -it is *strongly* recommended that you unload/disable the kernel's SPI -drivers *dw_spi* and *dw_spi_mmio* for the RPi5 or *spi_bmc2835* for the -RPi3 and RPi4. The hm2_spix hardware drivers attempt to unload the -kernel driver at startup if detected and restore it at exit if initially -loaded. However, there are no guarantees about the effectiveness of the -module unload/load actions. +If you do not require the spix_spidev driver you must disable the +kernel's SPI driver before *hm2_spix* will load. The conflicting module +is *spi_bcm2835* on the RPi3 / RPi4 and *dw_spi_mmio* (with its +dependency *dw_spi*) on the RPi5. The driver detects the kernel module +at startup and refuses to load with "Kernel SPI driver ... is loaded +and conflicts" rather than fight the kernel for the bus. + +To disable on RPi3 / RPi4, run *raspi-config* and pick +Interface Options -> SPI -> Disable, then reboot. + +To disable on RPi5, blacklist both kernel modules. Create +*/etc/modprobe.d/blacklist-linuxcnc.conf* containing: + +---- +blacklist dw_spi_mmio +blacklist dw_spi +---- + +Then reboot. If either module is built-in to the kernel rather than +loadable, use a kernel command-line override +(*modprobe.blacklist=dw_spi_mmio,dw_spi* in `/boot/firmware/cmdline.txt`). *Warning*: having both kernel and user-space SPI drivers installed can result in unexpected interactions and system instabilities. diff --git a/src/Makefile b/src/Makefile index 94fdadc607e..85802d561e2 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1047,7 +1047,6 @@ hm2_pci-objs := \ $(MATHSTUB) hm2_eth-objs := \ hal/drivers/mesa-hostmot2/hm2_eth.o \ - hal/drivers/mesa-hostmot2/eshellf.o \ $(MATHSTUB) hm2_spi-objs := \ hal/drivers/mesa-hostmot2/hm2_spi.o \ @@ -1056,7 +1055,7 @@ hm2_spi-objs := \ hm2_rpspi-objs := \ hal/drivers/mesa-hostmot2/hm2_rpspi.o \ hal/drivers/mesa-hostmot2/llio_info.o \ - hal/drivers/mesa-hostmot2/eshellf.o \ + hal/drivers/mesa-hostmot2/kmod_check.o \ $(MATHSTUB) hm2_spix-objs := \ hal/drivers/mesa-hostmot2/hm2_spix.o \ @@ -1064,7 +1063,7 @@ hm2_spix-objs := \ hal/drivers/mesa-hostmot2/spix_rpi3.o \ hal/drivers/mesa-hostmot2/spix_spidev.o \ hal/drivers/mesa-hostmot2/llio_info.o \ - hal/drivers/mesa-hostmot2/eshellf.o \ + hal/drivers/mesa-hostmot2/kmod_check.o \ $(MATHSTUB) hm2_modbus-objs := \ hal/drivers/mesa-hostmot2/hm2_modbus.o \ diff --git a/src/hal/drivers/mesa-hostmot2/eshellf.c b/src/hal/drivers/mesa-hostmot2/eshellf.c deleted file mode 100644 index 6e100e8438a..00000000000 --- a/src/hal/drivers/mesa-hostmot2/eshellf.c +++ /dev/null @@ -1,63 +0,0 @@ -/* - * This is a component for hostmot2 board drivers - * Copyright (c) 2013,2014,2020,2024 Michael Geszkiewicz , - * Jeff Epler - * B.Stultiens - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along with - * this program; if not, see . - */ - -#include -#include -#include -#include -#include -#include - -#include - -int shell(char *command) -{ - char *const argv[] = {"sh", "-c", command, NULL}; - pid_t pid; - int res = rtapi_spawn_as_root(&pid, "/bin/sh", NULL, NULL, argv, environ); - if(res < 0) - perror("rtapi_spawn_as_root"); - int status; - waitpid(pid, &status, 0); - if(WIFEXITED(status)) - return WEXITSTATUS(status); - else if(WIFSTOPPED(status)) - return WTERMSIG(status)+128; - return status; -} - -int eshellf(const char *errpfx, const char *fmt, ...) -{ - char commandbuf[1024]; - va_list ap; - va_start(ap, fmt); - vsnprintf(commandbuf, sizeof(commandbuf), fmt, ap); - va_end(ap); - - int res = shell(commandbuf); - if(res == EXIT_SUCCESS) - return 0; - - rtapi_print_msg(RTAPI_MSG_ERR, "%s: ERROR: Failed to execute '%s'\n", errpfx ? errpfx : "eshellf()", commandbuf); - return -EINVAL; -} - -/* vim: ts=4 - */ diff --git a/src/hal/drivers/mesa-hostmot2/hm2_rpspi.c b/src/hal/drivers/mesa-hostmot2/hm2_rpspi.c index e3e689665f1..792136e327e 100644 --- a/src/hal/drivers/mesa-hostmot2/hm2_rpspi.c +++ b/src/hal/drivers/mesa-hostmot2/hm2_rpspi.c @@ -41,8 +41,8 @@ #include "hostmot2-lowlevel.h" #include "hostmot2.h" #include "spi_common_rpspi.h" -#include "eshellf.h" #include "llio_info.h" +#include "kmod_check.h" #define HM2_LLIO_NAME "hm2_rpspi" @@ -1272,7 +1272,6 @@ static void hm2_rpspi_cleanup(void) peripheral_restore(); munmap(peripheralmem, peripheralsize); } - eshellf(HM2_LLIO_NAME, "/sbin/modprobe spi-bcm2835"); } /*************************************************/ @@ -1280,7 +1279,13 @@ int rtapi_app_main() { int ret; - eshellf(HM2_LLIO_NAME, "/sbin/rmmod spi_bcm2835"); + if(kernel_module_loaded("spi_bcm2835")) { + LL_ERR("Kernel SPI driver spi_bcm2835 is loaded and conflicts with " + HM2_LLIO_NAME ". Disable it with 'sudo raspi-config' " + "(Interface Options -> SPI -> Disable) and reboot. " + "See hm2_rpspi(9) for details.\n"); + return -EBUSY; + } if((comp_id = ret = hal_init("hm2_rpspi")) < 0) goto fail; diff --git a/src/hal/drivers/mesa-hostmot2/eshellf.h b/src/hal/drivers/mesa-hostmot2/kmod_check.c similarity index 64% rename from src/hal/drivers/mesa-hostmot2/eshellf.h rename to src/hal/drivers/mesa-hostmot2/kmod_check.c index 283b9f93427..ccda6407c54 100644 --- a/src/hal/drivers/mesa-hostmot2/eshellf.h +++ b/src/hal/drivers/mesa-hostmot2/kmod_check.c @@ -1,8 +1,6 @@ /* - * This is a component for hostmot2 board drivers - * Copyright (c) 2013,2014,2020,2024 Michael Geszkiewicz , - * Jeff Epler - * B.Stultiens + * Shared helper: check whether a kernel module is loaded. + * Copyright (c) 2026 Luca Toniolo * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free @@ -17,12 +15,15 @@ * You should have received a copy of the GNU General Public License along with * this program; if not, see . */ -#ifndef HAL_HM2_ESHELLF_H -#define HAL_HM2_ESHELLF_H -int shell(char *command); -int eshellf(const char *errpfx, const char *fmt, ...); +#include +#include -#endif -/* vim: ts=4 - */ +#include "kmod_check.h" + +int kernel_module_loaded(const char *name) +{ + char path[256]; + snprintf(path, sizeof(path), "/sys/module/%s", name); + return access(path, F_OK) == 0; +} diff --git a/src/hal/drivers/mesa-hostmot2/kmod_check.h b/src/hal/drivers/mesa-hostmot2/kmod_check.h new file mode 100644 index 00000000000..95b96690174 --- /dev/null +++ b/src/hal/drivers/mesa-hostmot2/kmod_check.h @@ -0,0 +1,40 @@ +/* + * Shared helper: check whether a kernel module is loaded. + * Copyright (c) 2026 Luca Toniolo + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, see . + */ +#ifndef HAL_HM2_KMOD_CHECK_H +#define HAL_HM2_KMOD_CHECK_H + +/* + * Check whether a kernel module is loaded (loadable or built-in). + * Returns non-zero when present, zero when absent. + * + * Implemented via /sys/module/, which the kernel populates for + * both loadable modules (initstate "live") and built-in ones (no + * initstate file). This catches the built-in case that /proc/modules + * misses; on a custom kernel where the conflicting driver is compiled + * in rather than modular, the check still fires. + * + * NOTE: pass the canonical underscore form of the module name. The + * kernel exposes modules in /sys/module/ with underscores only, so + * "spi-bcm2835" will not match "spi_bcm2835" on disk. modprobe/lsmod + * canonicalize for you; this helper does not. + */ +int kernel_module_loaded(const char *name); + +#endif +/* vim: ts=4 + */ diff --git a/src/hal/drivers/mesa-hostmot2/spix_rpi3.c b/src/hal/drivers/mesa-hostmot2/spix_rpi3.c index f82553421f7..3e700048af6 100644 --- a/src/hal/drivers/mesa-hostmot2/spix_rpi3.c +++ b/src/hal/drivers/mesa-hostmot2/spix_rpi3.c @@ -31,10 +31,10 @@ #include "hostmot2-lowlevel.h" -#include "eshellf.h" #include "spix.h" #include "dtcboards.h" #include "spi_common_rpspi.h" +#include "kmod_check.h" //#define RPSPI_DEBUG_PIN 23 // Define for pin-debugging @@ -120,7 +120,6 @@ spix_driver_t spix_driver_rpi3 = { .close = rpi3_close, }; -static int has_spi_module; // Set to non-zero when the kernel module spi_bcm2835 is loaded static int driver_enabled; // Set to non-zero when rpi3_setup() is successfully called static int port_probe_mask; // Which ports are requested @@ -724,7 +723,7 @@ static int rpi3_detect(const char *dtcs[]) /* * Setup the driver. - * - remove kernel spidev driver modules if detected + * - check for conflicting kernel SPI module * - map the I/O memory * - setup the GPIO pins and SPI peripheral(s) */ @@ -742,12 +741,15 @@ static int rpi3_setup(int probemask) port_probe_mask = probemask; // For peripheral_setup() and peripheral_restore() - // Now we know what platform we are running, remove kernel SPI module if - // detected - if((has_spi_module = (0 == shell("/usr/bin/grep -qw ^spi_bcm2835 /proc/modules")))) { - if(shell("/sbin/rmmod spi_bcm2835")) - LL_ERR("Unable to remove kernel SPI module spi_bcm2835. " - "Your system may become unstable using LinuxCNC with the " HM2_LLIO_NAME " driver.\n"); + // The kernel SPI driver conflicts with direct peripheral access. Fail at + // load if it is present so the user can disable it deliberately rather + // than running a half-working system. + if(kernel_module_loaded("spi_bcm2835")) { + LL_ERR("Kernel SPI driver spi_bcm2835 is loaded and conflicts with " + HM2_LLIO_NAME ". Disable it with 'sudo raspi-config' " + "(Interface Options -> SPI -> Disable) and reboot. " + "See hm2_spix(9) NOTES for details.\n"); + return -EBUSY; } spiclk_base = read_spiclkbase(); @@ -805,10 +807,6 @@ static int rpi3_cleanup(void) munmap(peripheralmem, peripheralsize); } - // Restore kernel SPI module if it was detected before - if(has_spi_module) - shell("/sbin/modprobe spi_bcm2835"); - driver_enabled = 0; return 0; } diff --git a/src/hal/drivers/mesa-hostmot2/spix_rpi5.c b/src/hal/drivers/mesa-hostmot2/spix_rpi5.c index 4648d40621c..9fbff7a8ab1 100644 --- a/src/hal/drivers/mesa-hostmot2/spix_rpi5.c +++ b/src/hal/drivers/mesa-hostmot2/spix_rpi5.c @@ -27,10 +27,10 @@ #include "hostmot2-lowlevel.h" -#include "eshellf.h" #include "spix.h" #include "dtcboards.h" #include "rp1dev.h" +#include "kmod_check.h" //#define RPSPI_DEBUG_PIN 23 // Define for pin-debugging @@ -123,7 +123,6 @@ typedef struct __spisave_t { static spisave_t spi0save; // Settings before our setup static spisave_t spi1save; -static int has_spi_module; // Set to non-zero when the kernel modules dw_spi and dw_spi_mmio are loaded static int driver_enabled; // Set to non-zero when rpi5_setup() is successfully called static int port_probe_mask; // Which ports are requested @@ -526,7 +525,7 @@ static int rpi5_detect(const char *dtcs[]) /* * Setup the driver. - * - remove kernel spidev driver modules if detected + * - check for conflicting kernel SPI module * - map the I/O memory * - setup the GPIO pins and SPI peripheral(s) */ @@ -541,12 +540,15 @@ static int rpi5_setup(int probemask) port_probe_mask = probemask; // For peripheral_setup() and peripheral_restore() - // Now we know what platform we are running, remove kernel SPI module if - // detected - if((has_spi_module = (0 == shell("/usr/bin/grep -qw ^dw_spi_mmio /proc/modules")))) { - if(shell("/sbin/rmmod dw_spi_mmio dw_spi")) - LL_ERR("Unable to remove kernel SPI modules dw_spi_mmio and dw_spi. " - "Your system may become unstable using LinuxCNC with the " HM2_LLIO_NAME " driver.\n"); + // The kernel SPI driver conflicts with direct peripheral access. Fail at + // load if it is present so the user can disable it deliberately rather + // than running a half-working system. + if(kernel_module_loaded("dw_spi_mmio")) { + LL_ERR("Kernel SPI driver dw_spi_mmio is loaded and conflicts with " + HM2_LLIO_NAME ". Blacklist dw_spi_mmio and dw_spi via " + "/etc/modprobe.d/blacklist-linuxcnc.conf and reboot. " + "See hm2_spix(9) NOTES for the exact recipe.\n"); + return -EBUSY; } // The IO address for the RPi5 is at a fixed address. No need to do fancy @@ -590,10 +592,6 @@ static int rpi5_cleanup(void) munmap(peripheralmem, peripheralsize); } - // Restore kernel SPI module if it was detected before - if(has_spi_module) - shell("/sbin/modprobe dw_spi_mmio"); - driver_enabled = 0; return 0; } From 4099ee2696fa3e2b12471437c0ae53642a8e9459 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sat, 16 May 2026 21:08:17 +0800 Subject: [PATCH 17/17] rtapi: gate setrlimit(MEMLOCK) warning on CAP_IPC_LOCK The previous DBG misled on the rootless path where the failure is expected and harmless: CAP_IPC_LOCK lets mlockall ignore RLIMIT_MEMLOCK regardless. Promote to WARN, fire only when the raise fails and CAP_IPC_LOCK is not held, i.e. when mlockall is actually going to fail. --- src/rtapi/uspace_rtapi_main.cc | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/rtapi/uspace_rtapi_main.cc b/src/rtapi/uspace_rtapi_main.cc index 5e4e438ca30..707bd199b8c 100644 --- a/src/rtapi/uspace_rtapi_main.cc +++ b/src/rtapi/uspace_rtapi_main.cc @@ -846,16 +846,27 @@ static void signal_handler(int sig, siginfo_t * /*si*/, void * /*uctx*/) { const static size_t PRE_ALLOC_SIZE = 1024 * 1024 * 32; const static struct rlimit unlimited = {RLIM_INFINITY, RLIM_INFINITY}; static void configure_memory() { - // Best-effort raise of the soft cap to the hard cap. Fails on - // unprivileged processes without CAP_SYS_RESOURCE or without a - // matching setrlimit; CAP_IPC_LOCK alone lets mlockall succeed - // regardless of the rlimit, so ignoring the failure is safe. + // Best-effort raise of the soft cap to the hard cap. Needs + // CAP_SYS_RESOURCE; absent that, CAP_IPC_LOCK lets mlockall succeed + // regardless of the rlimit. Only warn when neither path is open, + // i.e. mlockall is actually going to fail. + bool have_ipc_lock = false; +#ifdef __linux__ + cap_t caps = cap_get_proc(); + if (caps) { + cap_flag_value_t v; + if (cap_get_flag(caps, CAP_IPC_LOCK, CAP_EFFECTIVE, &v) == 0) + have_ipc_lock = (v == CAP_SET); + cap_free(caps); + } +#endif struct rlimit limit; if (getrlimit(RLIMIT_MEMLOCK, &limit) == 0) { limit.rlim_cur = limit.rlim_max; - if (setrlimit(RLIMIT_MEMLOCK, &limit) < 0) - rtapi_print_msg(RTAPI_MSG_DBG, - "setrlimit(RLIMIT_MEMLOCK) failed: %s\n", strerror(errno)); + if (setrlimit(RLIMIT_MEMLOCK, &limit) < 0 && !have_ipc_lock) + rtapi_print_msg(RTAPI_MSG_WARN, + "setrlimit(RLIMIT_MEMLOCK) failed and CAP_IPC_LOCK not held: %s. " + "mlockall will likely fail.\n", strerror(errno)); } int res = mlockall(MCL_CURRENT | MCL_FUTURE);