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@, 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" diff --git a/docs/src/man/man9/hm2_eth.9.adoc b/docs/src/man/man9/hm2_eth.9.adoc index 7cf1b3015a9..cdb29cedddd 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,16 @@ ____ *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 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 @@ -146,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/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 c564f77e843..85802d561e2 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,25 @@ 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 +# 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: +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. @@ -1022,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 \ @@ -1031,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 \ @@ -1039,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/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"); 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_eth.c b/src/hal/drivers/mesa-hostmot2/hm2_eth.c index 8cb23fe40b7..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; @@ -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; @@ -463,38 +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(!chain_exists()) { - int res = shell(IPTABLES " -N " CHAIN); - if(res != EXIT_SUCCESS) { - LL_PRINT("ERROR: Failed to create iptables chain "CHAIN); + if(no_iptables) { + LL_PRINT("Skipping iptables setup (no_iptables=1); " + "configure firewall externally.\n"); + return (iptables_state = 0); + } + // 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); + } + // 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() { + 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) { @@ -530,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); @@ -579,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; } @@ -709,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; @@ -1619,7 +1678,7 @@ void rtapi_app_exit(void) { for(i = 0; i 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; } diff --git a/src/hal/utils/upci.c b/src/hal/utils/upci.c index 0a18f7b87b2..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 ) @@ -401,37 +412,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 ) 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 5b625ff03bc..a7981164b37 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,27 +353,38 @@ 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() { +// 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). +static inline 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; + if(uname(&u) < 0) return 0; + return strcasestr(u.version, "PREEMPT RT") != 0 + || strcasestr(u.version, "PREEMPT_RT") != 0; } #else -static int detect_preempt_rt() { +static inline 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. +static inline 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; @@ -383,6 +396,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; @@ -394,6 +408,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; @@ -404,22 +419,64 @@ static int detect_xenomai_evl() { } #endif -static int detect_env_override() { - char *p = getenv("LINUXCNC_FORCE_REALTIME"); - return p != NULL && atoi(p) != 0; -} +// 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 +// 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) { + rtapi_sched_fifo_last_errno = errno; + return 0; + } + if(sched_getparam(0, &old_param) < 0) { + rtapi_sched_fifo_last_errno = errno; + return 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)) + 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) { + rtapi_sched_fifo_last_errno = errno; 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); + 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 +// 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 @@ -433,7 +490,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); diff --git a/src/rtapi/uspace_rtapi_main.cc b/src/rtapi/uspace_rtapi_main.cc index 3a73c5e294e..707bd199b8c 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++) { @@ -840,13 +846,34 @@ 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. 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 && !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)); + } - 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.*/ @@ -900,7 +927,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) ); @@ -909,19 +936,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, @@ -985,9 +1014,77 @@ 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; - if (WithRoot::getEuid() != 0 || harden_rt() < 0) { + 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: 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); } else { WithRoot r; @@ -998,6 +1095,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); } }