From 23087aa718b3a21d17a5e22399769e183c3b3db9 Mon Sep 17 00:00:00 2001 From: rpm-build Date: Fri, 7 Aug 2026 13:20:17 +0200 Subject: [PATCH 4/5] Fix symlink following in event handler scripts Resolves: CVE-2026-54230 Event handler scripts write output files using shell redirections and Python open() which follow symlinks. An attacker who controls the dump directory can plant symlinks to redirect root-owned writes to arbitrary files such as /var/spool/cron/root. Replace shell redirections with dd(1) using oflag=nofollow and conv=excl flags, which mirror the O_NOFOLLOW|O_EXCL protection used by libreport's dd_save_text()/create_new_file_at(). For Python scripts, use os.open() with O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW via dir_fd for safe writes. Co-Authored-By: Claude Opus 4.6 --- src/daemon/abrt-handle-upload.in | 6 +++++- src/daemon/abrt_event.conf | 9 +++++---- src/plugins/abrt-action-analyze-core.in | 15 +++++++++++++++ src/plugins/abrt-action-analyze-vmcore.in | 12 +++++++++++- .../abrt-action-check-oops-for-alt-component.in | 16 ++++++++++++++++ .../abrt-action-check-oops-for-hw-error.in | 16 ++++++++++++++++ src/plugins/abrt-action-generate-machine-id | 17 +++++++++++++++-- src/plugins/abrt-action-list-dsos | 15 +++++++++++++++ src/plugins/ccpp_event.conf | 6 +++--- src/plugins/gconf_event.conf | 2 +- src/plugins/koops_event.conf | 2 +- src/plugins/machine-id_event.conf | 2 +- src/plugins/python3_event.conf | 2 +- src/plugins/python_event.conf | 2 +- src/plugins/smart_event.conf | 9 ++++++--- src/plugins/sosreport_event.conf | 2 +- src/plugins/vimrc_event.conf | 8 ++++---- src/plugins/vmcore_event.conf | 4 ++-- src/plugins/xorg_event.conf | 12 ++++++------ 19 files changed, 125 insertions(+), 32 deletions(-) diff --git a/src/daemon/abrt-handle-upload.in b/src/daemon/abrt-handle-upload.in index 469c2ae..53ac88d 100755 --- a/src/daemon/abrt-handle-upload.in +++ b/src/daemon/abrt-handle-upload.in @@ -38,7 +38,11 @@ def init_gettext(): import problem def write_bytes_to(filename, b, uid, gid, mode): - fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode) + try: + os.unlink(filename) + except FileNotFoundError: + pass + fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, mode) if fd >= 0: os.fchown(fd, uid, gid) os.write(fd, b) diff --git a/src/daemon/abrt_event.conf b/src/daemon/abrt_event.conf index 9c75a4e..834a8c8 100644 --- a/src/daemon/abrt_event.conf +++ b/src/daemon/abrt_event.conf @@ -67,20 +67,21 @@ EVENT=post-create remote!=1 # uid file is missing for problems visible to all users # (oops scanner is often set up to not create it). # Record username only if uid element is present: - if [ -f uid ]; then getent passwd "`cat uid`" | cut -d: -f1 >username; fi + if [ -f uid ]; then rm -f username && getent passwd "`cat uid`" | cut -d: -f1 | dd of=username oflag=nofollow conv=excl status=none; fi # Save cpuinfo because crashes in some components are # related to HW acceleration. The file must be captured for all crashes # because of the library vs. executable problem. + rm -f "$DUMP_DIR/cpuinfo" if command -v lscpu >/dev/null 2>&1; then # use lscpu if installed - lscpu > $DUMP_DIR/cpuinfo + lscpu | dd of="$DUMP_DIR/cpuinfo" oflag=nofollow conv=excl status=none else - cp /proc/cpuinfo $DUMP_DIR/cpuinfo + dd if=/proc/cpuinfo of="$DUMP_DIR/cpuinfo" oflag=nofollow conv=excl status=none fi # Record runlevel (if not yet done) and don't return non-0 if it fails: EVENT=post-create runlevel= remote!=1 - runlevel >runlevel 2>&1 + rm -f runlevel && runlevel 2>&1 | dd of=runlevel oflag=nofollow conv=excl status=none exit 0 # A dummy EVENT=post-create for uploaded problems. diff --git a/src/plugins/abrt-action-analyze-core.in b/src/plugins/abrt-action-analyze-core.in index 9aca379..c6f7b19 100644 --- a/src/plugins/abrt-action-analyze-core.in +++ b/src/plugins/abrt-action-analyze-core.in @@ -35,8 +35,23 @@ def error_msg_and_die(s): sys.stderr.write("%s\n" % s) sys.exit(1) +def safe_open_for_write(filename): + dir_fd = os.open(".", os.O_DIRECTORY | os.O_NOFOLLOW) + try: + try: + os.unlink(filename, dir_fd=dir_fd) + except FileNotFoundError: + pass + fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, + 0o640, dir_fd=dir_fd) + finally: + os.close(dir_fd) + return os.fdopen(fd, 'w') + def xopen(name, mode): try: + if mode == "w": + return safe_open_for_write(name) r = open(name, mode) except IOError as ex: error_msg_and_die("Can't open '%s': %s" % (name, ex)) diff --git a/src/plugins/abrt-action-analyze-vmcore.in b/src/plugins/abrt-action-analyze-vmcore.in index c91737f..32b4ae2 100644 --- a/src/plugins/abrt-action-analyze-vmcore.in +++ b/src/plugins/abrt-action-analyze-vmcore.in @@ -86,7 +86,17 @@ if __name__ == "__main__": if crash.returncode != 0: error_msg_and_die(_("Can't process {0}:\n{1}").format(vmcore, err)) - backtrace_file = open("backtrace", "w") + dir_fd = os.open(".", os.O_DIRECTORY | os.O_NOFOLLOW) + try: + try: + os.unlink("backtrace", dir_fd=dir_fd) + except FileNotFoundError: + pass + bt_fd = os.open("backtrace", os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, + 0o640, dir_fd=dir_fd) + finally: + os.close(dir_fd) + backtrace_file = os.fdopen(bt_fd, "w") dump_oops = Popen(["abrt-dump-oops", "-u", ".", dmesg_log], stdout=backtrace_file, stderr=PIPE, bufsize=-1) out, err = dump_oops.communicate() backtrace_file.close() diff --git a/src/plugins/abrt-action-check-oops-for-alt-component.in b/src/plugins/abrt-action-check-oops-for-alt-component.in index 3e8d853..963d2c4 100644 --- a/src/plugins/abrt-action-check-oops-for-alt-component.in +++ b/src/plugins/abrt-action-check-oops-for-alt-component.in @@ -45,8 +45,24 @@ def get_new_component(filename): f.close() return None +def safe_open_for_write(filename): + dir_fd = os.open(".", os.O_DIRECTORY | os.O_NOFOLLOW) + try: + try: + os.unlink(filename, dir_fd=dir_fd) + except FileNotFoundError: + pass + fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, + 0o640, dir_fd=dir_fd) + finally: + os.close(dir_fd) + return os.fdopen(fd, 'w') + + def open_or_die(filename, mode): try: + if mode == "w": + return safe_open_for_write(filename) f = open(filename, mode) except IOError as e: sys.stderr.write(str(e) + "\n") diff --git a/src/plugins/abrt-action-check-oops-for-hw-error.in b/src/plugins/abrt-action-check-oops-for-hw-error.in index f5e0afd..bc07767 100644 --- a/src/plugins/abrt-action-check-oops-for-hw-error.in +++ b/src/plugins/abrt-action-check-oops-for-hw-error.in @@ -47,8 +47,24 @@ def tail_with_search(filename, string, maxlen): return retval +def safe_open_for_write(filename): + dir_fd = os.open(".", os.O_DIRECTORY | os.O_NOFOLLOW) + try: + try: + os.unlink(filename, dir_fd=dir_fd) + except FileNotFoundError: + pass + fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, + 0o640, dir_fd=dir_fd) + finally: + os.close(dir_fd) + return os.fdopen(fd, 'w') + + def open_or_die(filename, mode): try: + if mode == "w": + return safe_open_for_write(filename) f = open(filename, mode) except IOError as e: sys.stderr.write(str(e) + "\n") diff --git a/src/plugins/abrt-action-generate-machine-id b/src/plugins/abrt-action-generate-machine-id index 3cb145a..8d465f5 100644 --- a/src/plugins/abrt-action-generate-machine-id +++ b/src/plugins/abrt-action-generate-machine-id @@ -25,7 +25,6 @@ import sys from argparse import ArgumentParser from subprocess import check_output import logging - import hashlib def generate_machine_id_dmidecode(): @@ -171,7 +170,21 @@ if __name__ == '__main__': if ARGS['output']: try: - with open(ARGS['output'], 'w') as fout: + output_path = ARGS['output'] + output_dir = os.path.dirname(output_path) or '.' + output_name = os.path.basename(output_path) + dir_fd = os.open(output_dir, os.O_DIRECTORY | os.O_NOFOLLOW) + try: + try: + os.unlink(output_name, dir_fd=dir_fd) + except FileNotFoundError: + pass + fd = os.open(output_name, + os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, + 0o640, dir_fd=dir_fd) + finally: + os.close(dir_fd) + with os.fdopen(fd, 'w') as fout: print_result(machineids, fout, not ARGS['noprefix']) except IOError as ex: logging.error("Could not open output file: {0}".format(str(ex))) diff --git a/src/plugins/abrt-action-list-dsos b/src/plugins/abrt-action-list-dsos index 8bf5415..bf828e4 100644 --- a/src/plugins/abrt-action-list-dsos +++ b/src/plugins/abrt-action-list-dsos @@ -17,8 +17,23 @@ def error_msg_and_die(s): sys.stderr.write("%s\n" % s) sys.exit(1) +def safe_open_for_write(filename): + dir_fd = os.open(".", os.O_DIRECTORY | os.O_NOFOLLOW) + try: + try: + os.unlink(filename, dir_fd=dir_fd) + except FileNotFoundError: + pass + fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, + 0o640, dir_fd=dir_fd) + finally: + os.close(dir_fd) + return os.fdopen(fd, 'w') + def xopen(name, mode): try: + if mode == "w": + return safe_open_for_write(name) r = open(name, mode) except IOError as e: error_msg_and_die("Can't open '%s': %s" % (name, e)) diff --git a/src/plugins/ccpp_event.conf b/src/plugins/ccpp_event.conf index e89e98b..a806609 100644 --- a/src/plugins/ccpp_event.conf +++ b/src/plugins/ccpp_event.conf @@ -36,7 +36,7 @@ EVENT=post-create type=CCpp remote!=1 user_log=$user_log$line$'\n' fi done <<< "$user_log_full" - test -n "${user_log::-1}" && printf "User Logs:\n--%s--\n" "$user_log" >$DUMP_DIR/var_log_messages + test -n "${user_log::-1}" && rm -f "$DUMP_DIR/var_log_messages" && printf "User Logs:\n--%s--\n" "$user_log" | dd of="$DUMP_DIR/var_log_messages" oflag=nofollow conv=excl status=none # Do not use '&&' here because if $user_log is the empty string # then the script does not continue to get the system logs { @@ -49,7 +49,7 @@ EVENT=post-create type=CCpp remote!=1 system_log=$system_log$line$'\n' fi done <<< "$system_log_full" - test -n "${system_log::-1}" && printf "System Logs:\n--%s--\n" "$system_log" >$DUMP_DIR/var_log_messages + test -n "${system_log::-1}" && rm -f "$DUMP_DIR/var_log_messages" && printf "System Logs:\n--%s--\n" "$system_log" | dd of="$DUMP_DIR/var_log_messages" oflag=nofollow conv=excl status=none # Always exit with true here, because the false at # the beginning would cause the post-create hook to remove # the current problem directory. @@ -71,7 +71,7 @@ EVENT=collect_xsession_errors type=CCpp dso_list~=.*/libX11.* test -r "$xsession_errors" || { echo "Can't read $xsession_errors"; exit 0; } executable=`cat executable` && base_executable=${executable##*/} && - grep -F -e "$base_executable" "$xsession_errors" | tail -999 >xsession_errors && + rm -f xsession_errors && grep -F -e "$base_executable" "$xsession_errors" | tail -999 | dd of=xsession_errors oflag=nofollow conv=excl status=none && echo "Element 'xsession_errors' saved" # TODO: can we still specify additional directories to search for debuginfos, diff --git a/src/plugins/gconf_event.conf b/src/plugins/gconf_event.conf index 2e1f8ae..1d37493 100644 --- a/src/plugins/gconf_event.conf +++ b/src/plugins/gconf_event.conf @@ -6,7 +6,7 @@ EVENT=collect_GConf type=CCpp dso_list~=.*/libgconf-2.* gconftool-2 --dir-exists=$gconfdir || { echo "GConf directory $gconfdir does not exist"; exit 0; } } && - gconftool-2 --recursive-list $gconfdir >gconf_subtree && + rm -f gconf_subtree && gconftool-2 --recursive-list $gconfdir | dd of=gconf_subtree oflag=nofollow conv=excl status=none && echo "Element 'gconf_subtree' saved" diff --git a/src/plugins/koops_event.conf b/src/plugins/koops_event.conf index 5e53723..a59eeee 100644 --- a/src/plugins/koops_event.conf +++ b/src/plugins/koops_event.conf @@ -4,7 +4,7 @@ EVENT=post-create type=Kerneloops remote!=1 if [ "$(cat /proc/sys/kernel/dmesg_restrict)" == "0" ]; then # >> instead of > is due to bugzilla.redhat.com/854266 # 'dmesg' file is required by check-oops-for-hw-error - dmesg >>dmesg + dmesg | dd of=dmesg oflag=nofollow,append conv=notrunc status=none abrt-action-check-oops-for-hw-error fi { diff --git a/src/plugins/machine-id_event.conf b/src/plugins/machine-id_event.conf index 2825911..76979ab 100644 --- a/src/plugins/machine-id_event.conf +++ b/src/plugins/machine-id_event.conf @@ -1,3 +1,3 @@ #if you want to include *machineid* in dump directories: EVENT=post-create remote!=1 - /usr/libexec/abrt-action-generate-machine-id -o $DUMP_DIR/machineid >>event_log 2>&1 || : + /usr/libexec/abrt-action-generate-machine-id -o $DUMP_DIR/machineid 2>&1 | dd of=event_log oflag=nofollow,append conv=notrunc status=none || : diff --git a/src/plugins/python3_event.conf b/src/plugins/python3_event.conf index 481a1c4..c317ec5 100644 --- a/src/plugins/python3_event.conf +++ b/src/plugins/python3_event.conf @@ -10,7 +10,7 @@ EVENT=post-create type=Python3 remote!=1 abrt-action-analyze-python # save Python3 package version for line in $(rpm -qf $(which $(cut -d' ' -f1 < cmdline)) 2>/dev/null); do - echo -n $line > interpreter + rm -f interpreter && echo -n $line | dd of=interpreter oflag=nofollow conv=excl status=none done EVENT=report_Bugzilla type=Python3 component!=anaconda diff --git a/src/plugins/python_event.conf b/src/plugins/python_event.conf index 1824364..2ec4cc7 100644 --- a/src/plugins/python_event.conf +++ b/src/plugins/python_event.conf @@ -10,7 +10,7 @@ EVENT=post-create type=Python remote!=1 abrt-action-analyze-python # save Python2 package version for line in $(rpm -qf $(which $(cut -d' ' -f1 < cmdline)) 2>/dev/null); do - echo -n $line > interpreter + rm -f interpreter && echo -n $line | dd of=interpreter oflag=nofollow conv=excl status=none done EVENT=report_Bugzilla type=Python component!=anaconda diff --git a/src/plugins/smart_event.conf b/src/plugins/smart_event.conf index 2a0efb5..75fc0fe 100644 --- a/src/plugins/smart_event.conf +++ b/src/plugins/smart_event.conf @@ -9,24 +9,27 @@ EVENT=post-create component=gnome-disk-utility remote!=1 which skdump >/dev/null 2>&1 || exit 0 + rm -f smart_data for f in /dev/[sh]d[a-z]; do test -e "$f" || continue skdump "$f" echo - done >smart_data + done | dd of=smart_data oflag=nofollow conv=excl status=none EVENT=post-create component=libatasmart remote!=1 which skdump >/dev/null 2>&1 || exit 0 + rm -f smart_data for f in /dev/[sh]d[a-z]; do test -e "$f" || continue skdump "$f" echo - done >smart_data + done | dd of=smart_data oflag=nofollow conv=excl status=none EVENT=post-create component=udisks remote!=1 which skdump >/dev/null 2>&1 || exit 0 + rm -f smart_data for f in /dev/[sh]d[a-z]; do test -e "$f" || continue skdump "$f" echo - done >smart_data + done | dd of=smart_data oflag=nofollow conv=excl status=none diff --git a/src/plugins/sosreport_event.conf b/src/plugins/sosreport_event.conf index de5dfa7..988af6c 100644 --- a/src/plugins/sosreport_event.conf +++ b/src/plugins/sosreport_event.conf @@ -11,7 +11,7 @@ EVENT=post-create remote!=1 --only=cups --only=logs --only=grub2 --only=cron --only=pci \ --only=auditd --only=selinux --only=lvm2 --only=sar \ --only=processor \ - >sosreport.log 2>&1 \ + 2>&1 | dd of=sosreport.log oflag=nofollow conv=excl status=none \ && { rm sosreport.log rm sosreport*.md5 diff --git a/src/plugins/vimrc_event.conf b/src/plugins/vimrc_event.conf index cef991a..e011f29 100644 --- a/src/plugins/vimrc_event.conf +++ b/src/plugins/vimrc_event.conf @@ -6,13 +6,13 @@ EVENT=collect_vimrc_user component=vim gvimrc=~/.gvimrc saved=none if [ -r $vimrc -a -f $vimrc ]; then - cp $vimrc user_vimrc || exit $? + rm -f user_vimrc && dd if="$vimrc" of=user_vimrc oflag=nofollow conv=excl status=none || exit $? saved="$saved, user_vimrc" else echo "File $vimrc not found" fi if [ -r $gvimrc -a -f $gvimrc ]; then - cp $gvimrc user_gvimrc || exit $? + rm -f user_gvimrc && dd if="$gvimrc" of=user_gvimrc oflag=nofollow conv=excl status=none || exit $? saved="$saved, user_gvimrc" else echo "File $gvimrc not found" @@ -24,13 +24,13 @@ EVENT=collect_vimrc_system component=vim gvimrc=/etc/gvimrc saved=none if [ -r $vimrc -a -f $vimrc ]; then - cp $vimrc system_vimrc || exit $? + rm -f system_vimrc && dd if="$vimrc" of=system_vimrc oflag=nofollow conv=excl status=none || exit $? saved="$saved, system_vimrc" else echo "File $vimrc not found" fi if [ -r $gvimrc -a -f $gvimrc ]; then - cp $gvimrc system_gvimrc || exit $? + rm -f system_gvimrc && dd if="$gvimrc" of=system_gvimrc oflag=nofollow conv=excl status=none || exit $? saved="$saved, system_gvimrc" else echo "File $gvimrc not found" diff --git a/src/plugins/vmcore_event.conf b/src/plugins/vmcore_event.conf index 61bc9d1..6592a9f 100644 --- a/src/plugins/vmcore_event.conf +++ b/src/plugins/vmcore_event.conf @@ -12,7 +12,7 @@ EVENT=post-create type=vmcore remote!=1 # MCE oopses don't have kernel version in them, # but it should be specified earlier in the log. k=`sed -n '/Linux version/ s/.*Linux version \([^ ]*\) .*/\1/p' vmcore-dmesg.txt | tail -n1` - test "$k" != "" && printf "%s" "$k" >kernel + test "$k" != "" && rm -f kernel && printf "%s" "$k" | dd of=kernel oflag=nofollow conv=excl status=none else # No vmcore-dmesg.txt, do it the hard way: abrt-action-analyze-vmcore || exit $? @@ -23,7 +23,7 @@ EVENT=post-create type=vmcore remote!=1 # Try creating it from dmesg_log (created by abrt-action-analyze-vmcore): test -f dmesg_log || exit 0 k=`sed -n '/Linux version/ s/.*Linux version \([^ ]*\) .*/\1/p' dmesg_log | tail -n1` - test "$k" != "" && printf "%s" "$k" >kernel + test "$k" != "" && rm -f kernel && printf "%s" "$k" | dd of=kernel oflag=nofollow conv=excl status=none fi ) # Do not fail the event (->do not delete problem dir) diff --git a/src/plugins/xorg_event.conf b/src/plugins/xorg_event.conf index 8d0d585..d183070 100644 --- a/src/plugins/xorg_event.conf +++ b/src/plugins/xorg_event.conf @@ -6,13 +6,13 @@ EVENT=post-create type=xorg remote!=1 # Get versions of binaries listed in Xorg backtrace abrt-action-list-dsos -m backtrace -o dso_list # - test -f /var/log/Xorg.0.log && cp /var/log/Xorg.0.log . - test -f /etc/X11/xorg.conf && cp /etc/X11/xorg.conf . - test -d /etc/X11/xorg.conf.d && tar czf etc_X11_xorg_conf_d.tar.gz /etc/X11/xorg.conf.d - test -d /usr/share/X11/xorg.conf.d && tar czf usr_share_xorg_conf_d.tar.gz /usr/share/X11/xorg.conf.d + test -f /var/log/Xorg.0.log && rm -f Xorg.0.log && dd if=/var/log/Xorg.0.log of=Xorg.0.log oflag=nofollow conv=excl status=none + test -f /etc/X11/xorg.conf && rm -f xorg.conf && dd if=/etc/X11/xorg.conf of=xorg.conf oflag=nofollow conv=excl status=none + test -d /etc/X11/xorg.conf.d && rm -f etc_X11_xorg_conf_d.tar.gz && tar cz /etc/X11/xorg.conf.d | dd of=etc_X11_xorg_conf_d.tar.gz oflag=nofollow conv=excl status=none + test -d /usr/share/X11/xorg.conf.d && rm -f usr_share_xorg_conf_d.tar.gz && tar cz /usr/share/X11/xorg.conf.d | dd of=usr_share_xorg_conf_d.tar.gz oflag=nofollow conv=excl status=none # - # >> instead of > is due to bugzilla.redhat.com/show_bug.cgi?id=854266 - dmesg >>dmesg + # append instead of overwrite is due to bugzilla.redhat.com/show_bug.cgi?id=854266 + dmesg | dd of=dmesg oflag=nofollow,append conv=notrunc status=none # # save lspci -vvv output? -- 2.55.0