grub-set-bootflag: Fix for CVE-2024-1048
(CVE-2024-1048) Resolves: #RHEL-20747 Signed-off-by: Nicolas Frayer <nfrayer@redhat.com>
This commit is contained in:
		
							parent
							
								
									dc354eb1d9
								
							
						
					
					
						commit
						624933c2c9
					
				
							
								
								
									
										146
									
								
								0332-grub-set-bootflag-Conservative-partial-fix-for-CVE-2.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								0332-grub-set-bootflag-Conservative-partial-fix-for-CVE-2.patch
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,146 @@ | ||||
| From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 | ||||
| From: Solar Designer <solar@openwall.com> | ||||
| Date: Tue, 6 Feb 2024 21:39:41 +0100 | ||||
| Subject: [PATCH] grub-set-bootflag: Conservative partial fix for CVE-2024-1048 | ||||
| 
 | ||||
| Following up on CVE-2019-14865 and taking a fresh look at | ||||
| grub2-set-bootflag now (through my work at CIQ on Rocky Linux), I saw some | ||||
| other ways in which users could still abuse this little program: | ||||
| 
 | ||||
| 1. After CVE-2019-14865 fix, grub2-set-bootflag no longer rewrites the | ||||
| grubenv file in-place, but writes into a temporary file and renames it | ||||
| over the original, checking for error returns from each call first. | ||||
| This prevents the original file truncation vulnerability, but it can | ||||
| leave the temporary file around if the program is killed before it can | ||||
| rename or remove the file.  There are still many ways to get the program | ||||
| killed, such as through RLIMIT_FSIZE triggering SIGXFSZ (tested, | ||||
| reliable) or by careful timing (tricky) of signals sent by process group | ||||
| leader, pty, pre-scheduled timers, SIGXCPU (probably not an exhaustive | ||||
| list).  Invoking the program multiple times fills up /boot (or if /boot | ||||
| is not separate, then it can fill up the root filesystem).  Since the | ||||
| files are tiny, the filesystem is likely to run out of free inodes | ||||
| before it'd run out of blocks, but the effect is similar - can't create | ||||
| new files after this point (but still can add data to existing files, | ||||
| such as logs). | ||||
| 
 | ||||
| 2. After CVE-2019-14865 fix, grub2-set-bootflag naively tries to protect | ||||
| itself from signals by becoming full root.  (This does protect it from | ||||
| signals sent by the user directly to the PID, but e.g. "kill -9 -1" by | ||||
| the user still works.)  A side effect of such "protection" is that it's | ||||
| possible to invoke more concurrent instances of grub2-set-bootflag than | ||||
| the user's RLIMIT_NPROC would normally permit (as specified e.g. in | ||||
| /etc/security/limits.conf, or say in Apache httpd's RLimitNPROC if | ||||
| grub2-set-bootflag would be abused by a website script), thereby | ||||
| exhausting system resources (e.g., bypassing RAM usage limit if | ||||
| RLIMIT_AS was also set). | ||||
| 
 | ||||
| 3. umask is inherited.  Again, due to how the CVE-2019-14865 fix creates | ||||
| a new file, and due to how mkstemp() works, this affects grubenv's new | ||||
| file permissions.  Luckily, mkstemp() forces them to be no more relaxed | ||||
| than 0600, but the user ends up being able to set them e.g. to 0. | ||||
| Luckily, at least in my testing GRUB still works fine even when the file | ||||
| has such (lack of) permissions. | ||||
| 
 | ||||
| This commit deals with the abuses above as follows: | ||||
| 
 | ||||
| 1. RLIMIT_FSIZE is pre-checked, so this specific way to get the process | ||||
| killed should no longer work.  However, this isn't a complete fix | ||||
| because there are other ways to get the process killed after it has | ||||
| created the temporary file. | ||||
| 
 | ||||
| The commit also fixes bug 1975892 ("RFE: grub2-set-bootflag should not | ||||
| write the grubenv when the flag being written is already set") and | ||||
| similar for "menu_show_once", which further reduces the abuse potential. | ||||
| 
 | ||||
| 2. RLIMIT_NPROC bypass should be avoided by not becoming full root (aka | ||||
| dropping the partial "kill protection"). | ||||
| 
 | ||||
| 3. A safe umask is set. | ||||
| 
 | ||||
| This is a partial fix (temporary files can still accumulate, but this is | ||||
| harder to trigger). | ||||
| 
 | ||||
| While at it, this commit also fixes potential 1- or 2-byte over-read of | ||||
| env[] if its content is malformed - this was not a security issue since the | ||||
| grubenv file is trusted input, and the fix is just for robustness. | ||||
| 
 | ||||
| Signed-off-by: Solar Designer <solar@openwall.com> | ||||
| ---
 | ||||
|  util/grub-set-bootflag.c | 29 ++++++++++++++++------------- | ||||
|  1 file changed, 16 insertions(+), 13 deletions(-) | ||||
| 
 | ||||
| diff --git a/util/grub-set-bootflag.c b/util/grub-set-bootflag.c
 | ||||
| index 3b4c25ca2ac6..5bbbef804391 100644
 | ||||
| --- a/util/grub-set-bootflag.c
 | ||||
| +++ b/util/grub-set-bootflag.c
 | ||||
| @@ -33,6 +33,8 @@
 | ||||
|  #include <stdlib.h> | ||||
|  #include <string.h> | ||||
|  #include <unistd.h> | ||||
| +#include <sys/stat.h>
 | ||||
| +#include <sys/resource.h>
 | ||||
|   | ||||
|  #include "progname.h" | ||||
|   | ||||
| @@ -57,12 +59,17 @@ static void usage(FILE *out)
 | ||||
|  int main(int argc, char *argv[]) | ||||
|  { | ||||
|    /* NOTE buf must be at least the longest bootflag length + 4 bytes */ | ||||
| -  char env[GRUBENV_SIZE + 1], buf[64], *s;
 | ||||
| +  char env[GRUBENV_SIZE + 1 + 2], buf[64], *s;
 | ||||
|    /* +1 for 0 termination, +6 for "XXXXXX" in tmp filename */ | ||||
|    char env_filename[PATH_MAX + 1], tmp_filename[PATH_MAX + 6 + 1]; | ||||
|    const char *bootflag; | ||||
|    int i, fd, len, ret; | ||||
|    FILE *f; | ||||
| +  struct rlimit rlim;
 | ||||
| +
 | ||||
| +  if (getrlimit(RLIMIT_FSIZE, &rlim) || rlim.rlim_cur < GRUBENV_SIZE || rlim.rlim_max < GRUBENV_SIZE)
 | ||||
| +    return 1;
 | ||||
| +  umask(077);
 | ||||
|   | ||||
|    if (argc != 2) | ||||
|      { | ||||
| @@ -94,20 +101,11 @@ int main(int argc, char *argv[])
 | ||||
|    len = strlen (bootflag); | ||||
|   | ||||
|    /* | ||||
| -   * Really become root. setuid avoids an user killing us, possibly leaking
 | ||||
| -   * the tmpfile. setgid avoids the new grubenv's gid being that of the user.
 | ||||
| +   * setegid avoids the new grubenv's gid being that of the user.
 | ||||
|     */ | ||||
| -  ret = setuid(0);
 | ||||
| -  if (ret)
 | ||||
| +  if (setegid(0))
 | ||||
|      { | ||||
| -      perror ("Error setuid(0) failed");
 | ||||
| -      return 1;
 | ||||
| -    }
 | ||||
| -
 | ||||
| -  ret = setgid(0);
 | ||||
| -  if (ret)
 | ||||
| -    {
 | ||||
| -      perror ("Error setgid(0) failed");
 | ||||
| +      perror ("Error setegid(0) failed");
 | ||||
|        return 1; | ||||
|      } | ||||
|   | ||||
| @@ -136,6 +134,9 @@ int main(int argc, char *argv[])
 | ||||
|   | ||||
|    /* 0 terminate env */ | ||||
|    env[GRUBENV_SIZE] = 0; | ||||
| +  /* not a valid flag value */
 | ||||
| +  env[GRUBENV_SIZE + 1] = 0;
 | ||||
| +  env[GRUBENV_SIZE + 2] = 0;
 | ||||
|   | ||||
|    if (strncmp (env, GRUB_ENVBLK_SIGNATURE, strlen (GRUB_ENVBLK_SIGNATURE))) | ||||
|      { | ||||
| @@ -171,6 +172,8 @@ int main(int argc, char *argv[])
 | ||||
|   | ||||
|    /* The grubenv is not 0 terminated, so memcpy the name + '=' , '1', '\n' */ | ||||
|    snprintf(buf, sizeof(buf), "%s=1\n", bootflag); | ||||
| +  if (!memcmp(s, buf, len + 3))
 | ||||
| +    return 0; /* nothing to do */
 | ||||
|    memcpy(s, buf, len + 3); | ||||
|   | ||||
|   | ||||
							
								
								
									
										187
									
								
								0333-grub-set-bootflag-More-complete-fix-for-CVE-2024-104.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								0333-grub-set-bootflag-More-complete-fix-for-CVE-2024-104.patch
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,187 @@ | ||||
| From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 | ||||
| From: Solar Designer <solar@openwall.com> | ||||
| Date: Tue, 6 Feb 2024 21:56:21 +0100 | ||||
| Subject: [PATCH] grub-set-bootflag: More complete fix for CVE-2024-1048 | ||||
| 
 | ||||
| Switch to per-user fixed temporary filenames along with a weird locking | ||||
| mechanism, which is explained in source code comments.  This is a more | ||||
| complete fix than the previous commit (temporary files can't accumulate). | ||||
| Unfortunately, it introduces new risks (by working on a temporary file | ||||
| shared between the user's invocations), which are _hopefully_ avoided by | ||||
| the patch's elaborate logic.  I actually got it wrong at first, which | ||||
| suggests that this logic is hard to reason about, and more errors or | ||||
| omissions are possible.  It also relies on the kernel's primitives' exact | ||||
| semantics to a greater extent (nothing out of the ordinary, though). | ||||
| 
 | ||||
| Remaining issues that I think cannot reasonably be fixed without a | ||||
| redesign (e.g., having per-flag files with nothing else in them) and | ||||
| without introducing new issues: | ||||
| 
 | ||||
| A. A user can still revert a concurrent user's attempt of setting the | ||||
| other flag - or of making other changes to grubenv by means other than | ||||
| this program. | ||||
| 
 | ||||
| B. One leftover temporary file per user is still possible. | ||||
| 
 | ||||
| Signed-off-by: Solar Designer <solar@openwall.com> | ||||
| ---
 | ||||
|  util/grub-set-bootflag.c | 95 ++++++++++++++++++++++++++++++++++++++++-------- | ||||
|  1 file changed, 79 insertions(+), 16 deletions(-) | ||||
| 
 | ||||
| diff --git a/util/grub-set-bootflag.c b/util/grub-set-bootflag.c
 | ||||
| index 5bbbef804391..514c4f9091ac 100644
 | ||||
| --- a/util/grub-set-bootflag.c
 | ||||
| +++ b/util/grub-set-bootflag.c
 | ||||
| @@ -33,6 +33,7 @@
 | ||||
|  #include <stdlib.h> | ||||
|  #include <string.h> | ||||
|  #include <unistd.h> | ||||
| +#include <sys/file.h>
 | ||||
|  #include <sys/stat.h> | ||||
|  #include <sys/resource.h> | ||||
|   | ||||
| @@ -60,15 +61,12 @@ int main(int argc, char *argv[])
 | ||||
|  { | ||||
|    /* NOTE buf must be at least the longest bootflag length + 4 bytes */ | ||||
|    char env[GRUBENV_SIZE + 1 + 2], buf[64], *s; | ||||
| -  /* +1 for 0 termination, +6 for "XXXXXX" in tmp filename */
 | ||||
| -  char env_filename[PATH_MAX + 1], tmp_filename[PATH_MAX + 6 + 1];
 | ||||
| +  /* +1 for 0 termination, +11 for ".%u" in tmp filename */
 | ||||
| +  char env_filename[PATH_MAX + 1], tmp_filename[PATH_MAX + 11 + 1];
 | ||||
|    const char *bootflag; | ||||
|    int i, fd, len, ret; | ||||
|    FILE *f; | ||||
| -  struct rlimit rlim;
 | ||||
|   | ||||
| -  if (getrlimit(RLIMIT_FSIZE, &rlim) || rlim.rlim_cur < GRUBENV_SIZE || rlim.rlim_max < GRUBENV_SIZE)
 | ||||
| -    return 1;
 | ||||
|    umask(077); | ||||
|   | ||||
|    if (argc != 2) | ||||
| @@ -105,7 +103,7 @@ int main(int argc, char *argv[])
 | ||||
|     */ | ||||
|    if (setegid(0)) | ||||
|      { | ||||
| -      perror ("Error setegid(0) failed");
 | ||||
| +      perror ("setegid(0) failed");
 | ||||
|        return 1; | ||||
|      } | ||||
|   | ||||
| @@ -176,19 +174,82 @@ int main(int argc, char *argv[])
 | ||||
|      return 0; /* nothing to do */ | ||||
|    memcpy(s, buf, len + 3); | ||||
|   | ||||
| +  struct rlimit rlim;
 | ||||
| +  if (getrlimit(RLIMIT_FSIZE, &rlim) || rlim.rlim_cur < GRUBENV_SIZE || rlim.rlim_max < GRUBENV_SIZE)
 | ||||
| +    {
 | ||||
| +      fprintf (stderr, "Resource limits undetermined or too low\n");
 | ||||
| +      return 1;
 | ||||
| +    }
 | ||||
| +
 | ||||
| +  /*
 | ||||
| +   * Here we work under the premise that we shouldn't write into the target
 | ||||
| +   * file directly because we might not be able to have all of our changes
 | ||||
| +   * written completely and atomically.  That was CVE-2019-14865, known to
 | ||||
| +   * have been triggerable via RLIMIT_FSIZE.  While we've dealt with that
 | ||||
| +   * specific attack via the check above, there may be other possibilities.
 | ||||
| +   */
 | ||||
|   | ||||
|    /* | ||||
|     * Create a tempfile for writing the new env.  Use the canonicalized filename | ||||
|     * for the template so that the tmpfile is in the same dir / on same fs. | ||||
| +   *
 | ||||
| +   * We now use per-user fixed temporary filenames, so that a user cannot cause
 | ||||
| +   * multiple files to accumulate.
 | ||||
| +   *
 | ||||
| +   * We don't use O_EXCL so that a stale temporary file doesn't prevent further
 | ||||
| +   * usage of the program by the user.
 | ||||
|     */ | ||||
| -  snprintf(tmp_filename, sizeof(tmp_filename), "%sXXXXXX", env_filename);
 | ||||
| -  fd = mkstemp(tmp_filename);
 | ||||
| +  snprintf(tmp_filename, sizeof(tmp_filename), "%s.%u", env_filename, getuid());
 | ||||
| +  fd = open(tmp_filename, O_CREAT | O_WRONLY, 0600);
 | ||||
|    if (fd == -1) | ||||
|      { | ||||
|        perror ("Creating tmpfile failed"); | ||||
|        return 1; | ||||
|      } | ||||
|   | ||||
| +  /*
 | ||||
| +   * The lock prevents the same user from reaching further steps ending in
 | ||||
| +   * rename() concurrently, in which case the temporary file only partially
 | ||||
| +   * written by one invocation could be renamed to the target file by another.
 | ||||
| +   *
 | ||||
| +   * The lock also guards the slow fsync() from concurrent calls.  After the
 | ||||
| +   * first time that and the rename() complete, further invocations for the
 | ||||
| +   * same flag become no-ops.
 | ||||
| +   *
 | ||||
| +   * We lock the temporary file rather than the target file because locking the
 | ||||
| +   * latter would allow any user having SIGSTOP'ed their process to make all
 | ||||
| +   * other users' invocations fail (or lock up if we'd use blocking mode).
 | ||||
| +   *
 | ||||
| +   * We use non-blocking mode (LOCK_NB) because the lock having been taken by
 | ||||
| +   * another process implies that the other process would normally have already
 | ||||
| +   * renamed the file to target by the time it releases the lock (and we could
 | ||||
| +   * acquire it), so we'd be working directly on the target if we proceeded,
 | ||||
| +   * which is undesirable, and we'd kind of fail on the already-done rename.
 | ||||
| +   */
 | ||||
| +  if (flock(fd, LOCK_EX | LOCK_NB))
 | ||||
| +    {
 | ||||
| +      perror ("Locking tmpfile failed");
 | ||||
| +      return 1;
 | ||||
| +    }
 | ||||
| +
 | ||||
| +  /*
 | ||||
| +   * Deal with the potential that another invocation proceeded all the way to
 | ||||
| +   * rename() and process exit while we were between open() and flock().
 | ||||
| +   */
 | ||||
| +  {
 | ||||
| +    struct stat st1, st2;
 | ||||
| +    if (fstat(fd, &st1) || stat(tmp_filename, &st2))
 | ||||
| +      {
 | ||||
| +        perror ("stat of tmpfile failed");
 | ||||
| +        return 1;
 | ||||
| +      }
 | ||||
| +    if (st1.st_dev != st2.st_dev || st1.st_ino != st2.st_ino)
 | ||||
| +      {
 | ||||
| +        fprintf (stderr, "Another invocation won race\n");
 | ||||
| +        return 1;
 | ||||
| +      }
 | ||||
| +  }
 | ||||
| +
 | ||||
|    f = fdopen (fd, "w"); | ||||
|    if (!f) | ||||
|      { | ||||
| @@ -213,6 +274,14 @@ int main(int argc, char *argv[])
 | ||||
|        return 1;      | ||||
|      } | ||||
|   | ||||
| +  ret = ftruncate (fileno (f), GRUBENV_SIZE);
 | ||||
| +  if (ret)
 | ||||
| +    {
 | ||||
| +      perror ("Error truncating tmpfile");
 | ||||
| +      unlink(tmp_filename);
 | ||||
| +      return 1;
 | ||||
| +    }
 | ||||
| +
 | ||||
|    ret = fsync (fileno (f)); | ||||
|    if (ret) | ||||
|      { | ||||
| @@ -221,15 +290,9 @@ int main(int argc, char *argv[])
 | ||||
|        return 1; | ||||
|      } | ||||
|   | ||||
| -  ret = fclose (f);
 | ||||
| -  if (ret)
 | ||||
| -    {
 | ||||
| -      perror ("Error closing tmpfile");
 | ||||
| -      unlink(tmp_filename);
 | ||||
| -      return 1;
 | ||||
| -    }
 | ||||
| -
 | ||||
|    /* | ||||
| +   * We must not close the file before rename() as that would remove the lock.
 | ||||
| +   *
 | ||||
|     * And finally rename the tmpfile with the new env over the old env, the | ||||
|     * linux kernel guarantees that this is atomic (from a syscall pov). | ||||
|     */ | ||||
| @ -0,0 +1,36 @@ | ||||
| From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 | ||||
| From: Solar Designer <solar@openwall.com> | ||||
| Date: Tue, 6 Feb 2024 22:05:45 +0100 | ||||
| Subject: [PATCH] grub-set-bootflag: Exit calmly when not running as root | ||||
| 
 | ||||
| Exit calmly when not installed SUID root and invoked by non-root.  This | ||||
| allows installing user/grub-boot-success.service unconditionally while | ||||
| supporting non-SUID installation of the program for some limited usage. | ||||
| 
 | ||||
| Signed-off-by: Solar Designer <solar@openwall.com> | ||||
| ---
 | ||||
|  util/grub-set-bootflag.c | 11 +++++++++++ | ||||
|  1 file changed, 11 insertions(+) | ||||
| 
 | ||||
| diff --git a/util/grub-set-bootflag.c b/util/grub-set-bootflag.c
 | ||||
| index 514c4f9091ac..31a868aeca8a 100644
 | ||||
| --- a/util/grub-set-bootflag.c
 | ||||
| +++ b/util/grub-set-bootflag.c
 | ||||
| @@ -98,6 +98,17 @@ int main(int argc, char *argv[])
 | ||||
|    bootflag = bootflags[i]; | ||||
|    len = strlen (bootflag); | ||||
|   | ||||
| +  /*
 | ||||
| +   * Exit calmly when not installed SUID root and invoked by non-root.  This
 | ||||
| +   * allows installing user/grub-boot-success.service unconditionally while
 | ||||
| +   * supporting non-SUID installation of the program for some limited usage.
 | ||||
| +   */
 | ||||
| +  if (geteuid())
 | ||||
| +    {
 | ||||
| +      printf ("grub-set-bootflag not running as root, no action taken\n");
 | ||||
| +      return 0;
 | ||||
| +    }
 | ||||
| +
 | ||||
|    /* | ||||
|     * setegid avoids the new grubenv's gid being that of the user. | ||||
|     */ | ||||
| @ -329,3 +329,6 @@ Patch0328: 0328-grub2-mkconfig-Pass-all-boot-params-when-used-by-ana.patch | ||||
| Patch0329: 0329-kern-ieee1275-init-ppc64-Restrict-high-memory-in-pre.patch | ||||
| Patch0330: 0330-normal-Remove-grub_env_set-prefix-in-grub_try_normal.patch | ||||
| Patch0331: 0331-search-command-add-flag-to-only-search-root-dev.patch | ||||
| Patch0332: 0332-grub-set-bootflag-Conservative-partial-fix-for-CVE-2.patch | ||||
| Patch0333: 0333-grub-set-bootflag-More-complete-fix-for-CVE-2024-104.patch | ||||
| Patch0334: 0334-grub-set-bootflag-Exit-calmly-when-not-running-as-ro.patch | ||||
|  | ||||
| @ -16,7 +16,7 @@ | ||||
| Name:		grub2 | ||||
| Epoch:		1 | ||||
| Version:	2.06 | ||||
| Release:	74%{?dist} | ||||
| Release:	75%{?dist} | ||||
| Summary:	Bootloader with support for Linux, Multiboot and more | ||||
| License:	GPLv3+ | ||||
| URL:		http://www.gnu.org/software/grub/ | ||||
| @ -533,6 +533,11 @@ mv ${EFI_HOME}/grub.cfg.stb ${EFI_HOME}/grub.cfg | ||||
| %endif | ||||
| 
 | ||||
| %changelog | ||||
| * Wed Feb 7 2024 Nicolas Frayer <nfrayer@redhat.com> - 2.06-75 | ||||
| - grub-set-bootflag: Fix for CVE-2024-1048 | ||||
| - (CVE-2024-1048) | ||||
| - Resolves: #RHEL-20747 | ||||
| 
 | ||||
| * Mon Feb 5 2024 Nicolas Frayer <nfrayer@redhat.com> - 2.06-74 | ||||
| - Don't run 20-grub.install for UKIs | ||||
| - Resolves: #RHEL-21368 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user