mirror of
https://sourceware.org/git/glibc.git
synced 2026-10-28 10:25:13 +08:00
libio: validate the wide vtable via a bounds-checked index
Calls dispatched through _IO_wide_data._wide_vtable (the WJUMP* macros) were not validated, unlike the narrow vtable which goes through IO_validate_vtable. This asymmetry is the primitive used by the FSOP family known publicly as "House of Apple 2" / angry-FSROP: point the checked narrow vtable at _IO_wfile_jumps to pass the existing check, then hide a forged function pointer in the unchecked wide vtable, reached via _IO_wfile_overflow -> _IO_wdoallocbuf -> _IO_WDOALLOCATE. Following Florian Weimer's suggestion, store the index of the wide jump table into __io_vtables rather than a raw pointer, and bounds-check the index on use. The field is internal, so there is no ABI compatibility concern. An attacker who overwrites _wide_vtable_index can only select a value that is either in range (a legitimate table) or out of range (rejected). This is simpler than pointer mangling: no pointer guard dependency, and the standard streams can be statically initialized with the constant index IO_WFILE_JUMPS, so no startup fix-up is needed. An out-of-range index is always a genuine error on the wide path: the wide vtable is only ever set to an internal table, and the foreign vtable acceptance in _IO_vtable_check applies to the narrow vtable of the standard streams, not to the wide dispatch. The check therefore terminates directly with __libc_fatal, which does not return, so a bad index can never reach the array access. The two spots that copy the wide vtable into the narrow vtable slot resolve and bounds-check the index first. Add tst-wide-vtable-check to verify that an out-of-range wide vtable index aborts the process and that legitimate wide I/O is unaffected. Signed-off-by: Alessandro Schino <7991aleschino@gmail.com> Reviewed-by: Avinal Kumar <avinal.xlvii@gmail.com> Reviewed-by: Adhemerval Zanella <adhemerval.zanella@linaro.org>
This commit is contained in:
1 parent
506ea57086
commit
603a7e4b5c
11 files changed
+187
-18
No files matched your search
+1
-1
@@ -170,7 +170,7 @@ $(objpfx)tst-popen-fork: $(shared-thread-library)
|
||||
|
||||
$(objpfx)tst-file-init-race: $(shared-thread-library)
|
||||
|
||||
tests-internal = tst-vtables tst-vtables-interposed
|
||||
tests-internal = tst-vtables tst-vtables-interposed tst-wide-vtable-check
|
||||
|
||||
ifeq (yes,$(build-shared))
|
||||
# Add test-fopenloc only if shared library is enabled since it depends on
|
||||
|
||||
+7
-6
@@ -398,7 +398,8 @@ _IO_new_file_fopen (FILE *fp, const char *filename, const char *mode,
|
||||
cc->__cd_out.step_data.__statep = &result->_wide_data->_IO_state;
|
||||
|
||||
/* From now on use the wide character callback functions. */
|
||||
_IO_JUMPS_FILE_plus (fp) = fp->_wide_data->_wide_vtable;
|
||||
_IO_JUMPS_FILE_plus (fp)
|
||||
= IO_wide_validate_index (fp->_wide_data->_wide_vtable_index);
|
||||
|
||||
/* Set the mode now. */
|
||||
result->_mode = 1;
|
||||
@@ -451,7 +452,7 @@ _IO_file_setbuf_mmap (FILE *fp, char *p, ssize_t len)
|
||||
|
||||
/* Change the function table. */
|
||||
_IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps;
|
||||
fp->_wide_data->_wide_vtable = &_IO_wfile_jumps;
|
||||
_IO_WIDE_JUMPS_FUNC_UPDATE (fp, &_IO_wfile_jumps);
|
||||
|
||||
/* And perform the normal operation. */
|
||||
result = _IO_new_file_setbuf (fp, p, len);
|
||||
@@ -460,7 +461,7 @@ _IO_file_setbuf_mmap (FILE *fp, char *p, ssize_t len)
|
||||
if (result == NULL)
|
||||
{
|
||||
_IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps_mmap;
|
||||
fp->_wide_data->_wide_vtable = &_IO_wfile_jumps_mmap;
|
||||
_IO_WIDE_JUMPS_FUNC_UPDATE (fp, &_IO_wfile_jumps_mmap);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -683,7 +684,7 @@ mmap_remap_check (FILE *fp)
|
||||
_IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps;
|
||||
else
|
||||
_IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps;
|
||||
fp->_wide_data->_wide_vtable = &_IO_wfile_jumps;
|
||||
_IO_WIDE_JUMPS_FUNC_UPDATE (fp, &_IO_wfile_jumps);
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -753,7 +754,7 @@ decide_maybe_mmap (FILE *fp)
|
||||
_IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps_mmap;
|
||||
else
|
||||
_IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps_mmap;
|
||||
fp->_wide_data->_wide_vtable = &_IO_wfile_jumps_mmap;
|
||||
_IO_WIDE_JUMPS_FUNC_UPDATE (fp, &_IO_wfile_jumps_mmap);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -766,7 +767,7 @@ decide_maybe_mmap (FILE *fp)
|
||||
_IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps;
|
||||
else
|
||||
_IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps;
|
||||
fp->_wide_data->_wide_vtable = &_IO_wfile_jumps;
|
||||
_IO_WIDE_JUMPS_FUNC_UPDATE (fp, &_IO_wfile_jumps);
|
||||
}
|
||||
|
||||
int
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ freopen (const char *filename, const char *mode, FILE *fp)
|
||||
_IO_file_close_maybe_unlink (fp, false);
|
||||
_IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps;
|
||||
if (_IO_vtable_offset (fp) == 0 && fp->_wide_data != NULL)
|
||||
fp->_wide_data->_wide_vtable = &_IO_wfile_jumps;
|
||||
_IO_WIDE_JUMPS_FUNC_UPDATE (fp, &_IO_wfile_jumps);
|
||||
fp->_flags2 &= ~(_IO_FLAGS2_MMAP
|
||||
| _IO_FLAGS2_NOTCANCEL
|
||||
| _IO_FLAGS2_CLOEXEC);
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ freopen64 (const char *filename, const char *mode, FILE *fp)
|
||||
_IO_file_close_maybe_unlink (fp, false);
|
||||
_IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps;
|
||||
if (_IO_vtable_offset (fp) == 0 && fp->_wide_data != NULL)
|
||||
fp->_wide_data->_wide_vtable = &_IO_wfile_jumps;
|
||||
_IO_WIDE_JUMPS_FUNC_UPDATE (fp, &_IO_wfile_jumps);
|
||||
fp->_flags2 &= ~(_IO_FLAGS2_MMAP
|
||||
| _IO_FLAGS2_NOTCANCEL
|
||||
| _IO_FLAGS2_CLOEXEC);
|
||||
|
||||
+1
-1
@@ -604,7 +604,7 @@ _IO_no_init (FILE *fp, int flags, int orientation,
|
||||
fp->_wide_data->_IO_backup_base = NULL;
|
||||
fp->_wide_data->_IO_save_end = NULL;
|
||||
|
||||
fp->_wide_data->_wide_vtable = jmp;
|
||||
_IO_WIDE_JUMPS_FUNC_UPDATE (fp, jmp);
|
||||
}
|
||||
else
|
||||
/* Cause predictable crash when a wide function is called on a byte
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ __fopen_maybe_mmap (FILE *fp)
|
||||
_IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps_maybe_mmap;
|
||||
else
|
||||
_IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps_maybe_mmap;
|
||||
fp->_wide_data->_wide_vtable = &_IO_wfile_jumps_maybe_mmap;
|
||||
_IO_WIDE_JUMPS_FUNC_UPDATE (fp, &_IO_wfile_jumps_maybe_mmap);
|
||||
}
|
||||
#endif
|
||||
return fp;
|
||||
|
||||
+2
-1
@@ -96,7 +96,8 @@ _IO_fwide (FILE *fp, int mode)
|
||||
}
|
||||
|
||||
/* From now on use the wide character callback functions. */
|
||||
_IO_JUMPS_FILE_plus (fp) = fp->_wide_data->_wide_vtable;
|
||||
_IO_JUMPS_FILE_plus (fp)
|
||||
= IO_wide_validate_index (fp->_wide_data->_wide_vtable_index);
|
||||
}
|
||||
|
||||
/* Set the mode now. */
|
||||
|
||||
+8
-1
@@ -145,7 +145,14 @@ struct _IO_wide_data
|
||||
|
||||
wchar_t _shortbuf[1];
|
||||
|
||||
const struct _IO_jump_t *_wide_vtable;
|
||||
/* Index into __io_vtables identifying the wide jump table, instead of
|
||||
a raw pointer. Storing an index and bounds-checking it on use (see
|
||||
_IO_WIDE_JUMPS_FUNC in libioP.h) prevents an attacker who can
|
||||
overwrite this field from redirecting wide I/O to an arbitrary
|
||||
address (the "House of Apple 2" / FSROP primitive), since the wide
|
||||
vtable dispatch was previously unchecked. This field is internal
|
||||
and not part of the ABI. */
|
||||
unsigned int _wide_vtable_index;
|
||||
};
|
||||
|
||||
struct _IO_FILE_plus;
|
||||
|
||||
+46
-3
@@ -100,8 +100,10 @@
|
||||
#define _IO_JUMPS(THIS) (THIS)->vtable
|
||||
#define _IO_JUMPS_FILE_plus(THIS) \
|
||||
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
|
||||
#define _IO_WIDE_JUMPS(THIS) \
|
||||
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
|
||||
/* Raw accessor for the stored wide vtable index. */
|
||||
#define _IO_WIDE_JUMPS_INDEX(THIS) \
|
||||
(_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data) \
|
||||
->_wide_vtable_index)
|
||||
#define _IO_CHECK_WIDE(THIS) \
|
||||
(_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data) != NULL)
|
||||
|
||||
@@ -120,7 +122,13 @@
|
||||
(_IO_JUMPS_FILE_plus (THIS) = (VTABLE))
|
||||
# define _IO_vtable_offset(THIS) 0
|
||||
#endif
|
||||
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
|
||||
/* Look up the wide vtable by its stored index, bounds-checked. */
|
||||
#define _IO_WIDE_JUMPS_FUNC(THIS) \
|
||||
(IO_wide_validate_index (_IO_WIDE_JUMPS_INDEX (THIS)))
|
||||
/* Store the index of VTABLE (a pointer into __io_vtables) into the wide
|
||||
vtable slot. Replaces every direct "_wide_vtable = ..." assignment. */
|
||||
#define _IO_WIDE_JUMPS_FUNC_UPDATE(THIS, VTABLE) \
|
||||
(_IO_WIDE_JUMPS_INDEX (THIS) = IO_wide_vtable_to_index (VTABLE))
|
||||
#define JUMP_FIELD(TYPE, NAME) TYPE NAME
|
||||
#define JUMP0(FUNC, THIS) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS)
|
||||
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
|
||||
@@ -1050,6 +1058,41 @@ IO_validate_vtable (const struct _IO_jump_t *vtable)
|
||||
return vtable;
|
||||
}
|
||||
|
||||
#if IS_IN (libc)
|
||||
/* Wide vtable hardening.
|
||||
|
||||
The wide jump table reached through the WJUMP* macros is identified by
|
||||
an index into __io_vtables (stored in _IO_wide_data._wide_vtable_index)
|
||||
rather than by a raw pointer. On use the index is bounds-checked and
|
||||
resolved to &__io_vtables[index]. An attacker who overwrites the field
|
||||
can only select a value that is either in range (a legitimate table) or
|
||||
out of range (rejected). This closes the House of Apple 2 / FSROP
|
||||
primitive, where the wide vtable dispatch was previously unchecked. */
|
||||
|
||||
/* Resolve a stored wide vtable index to its jump table, bounds-checked. */
|
||||
static inline const struct _IO_jump_t *
|
||||
IO_wide_validate_index (unsigned int index)
|
||||
{
|
||||
if (__glibc_unlikely (index >= IO_VTABLES_NUM))
|
||||
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
|
||||
return &__io_vtables[index];
|
||||
}
|
||||
|
||||
/* Convert a wide vtable pointer (which must point at the start of a table
|
||||
inside __io_vtables) to its index for storage. */
|
||||
static inline unsigned int
|
||||
IO_wide_vtable_to_index (const struct _IO_jump_t *vtable)
|
||||
{
|
||||
uintptr_t offset = (uintptr_t) vtable - (uintptr_t) &__io_vtables;
|
||||
/* The pointer must be aligned to the start of a jump table and lie
|
||||
within the section; otherwise the stored index would be bogus. */
|
||||
if (__glibc_unlikely (offset >= IO_VTABLES_LEN
|
||||
|| offset % sizeof (struct _IO_jump_t) != 0))
|
||||
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
|
||||
return offset / sizeof (struct _IO_jump_t);
|
||||
}
|
||||
#endif /* IS_IN (libc) */
|
||||
|
||||
/* In case of an allocation failure, we resort to using the fixed buffer
|
||||
_SHORT_BACKUPBUF. Free PTR unless it points to that buffer. */
|
||||
static __always_inline void
|
||||
|
||||
+2
-2
@@ -36,14 +36,14 @@
|
||||
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
|
||||
static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer; \
|
||||
static struct _IO_wide_data _IO_wide_data_##FD \
|
||||
= { ._wide_vtable = &_IO_wfile_jumps }; \
|
||||
= { ._wide_vtable_index = IO_WFILE_JUMPS }; \
|
||||
struct _IO_FILE_plus NAME \
|
||||
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \
|
||||
&_IO_file_jumps};
|
||||
#else
|
||||
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
|
||||
static struct _IO_wide_data _IO_wide_data_##FD \
|
||||
= { ._wide_vtable = &_IO_wfile_jumps }; \
|
||||
= { ._wide_vtable_index = IO_WFILE_JUMPS }; \
|
||||
struct _IO_FILE_plus NAME \
|
||||
= {FILEBUF_LITERAL(CHAIN, FLAGS, FD, &_IO_wide_data_##FD), \
|
||||
&_IO_file_jumps};
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/* Test that a corrupted wide vtable index (_IO_wide_data._wide_vtable_index)
|
||||
is detected and the process is terminated. This exercises the hardening
|
||||
that closes the House of Apple 2 / FSROP primitive, where the wide vtable
|
||||
dispatch was previously unchecked.
|
||||
Copyright (C) 2026 Free Software Foundation, Inc.
|
||||
This file is part of the GNU C Library.
|
||||
|
||||
The GNU C Library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
The GNU C Library 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
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with the GNU C Library; if not, see
|
||||
<https://www.gnu.org/licenses/>. */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <wchar.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
|
||||
#include <libioP.h>
|
||||
#include <support/capture_subprocess.h>
|
||||
#include <support/check.h>
|
||||
#include <support/support.h>
|
||||
|
||||
/* The fatal message printed by __libc_fatal on detection. */
|
||||
static const char expected_message[]
|
||||
= "Fatal error: glibc detected an invalid stdio handle\n";
|
||||
|
||||
/* Open a wide-oriented stream backed by a temporary file and return it.
|
||||
The stream is oriented wide so that subsequent wide operations follow
|
||||
the wide vtable dispatch path. */
|
||||
static FILE *
|
||||
open_wide_stream (void)
|
||||
{
|
||||
FILE *fp = tmpfile ();
|
||||
TEST_VERIFY_EXIT (fp != NULL);
|
||||
/* Orient the stream towards wide characters. */
|
||||
TEST_VERIFY_EXIT (fwide (fp, 1) > 0);
|
||||
return fp;
|
||||
}
|
||||
|
||||
/* Callback: overwrite the wide vtable index with an out-of-range value
|
||||
(as an attacker who can write the field would) and trigger a wide
|
||||
operation. The bounds check in IO_wide_validate_index rejects it and the
|
||||
dispatch aborts. */
|
||||
static void
|
||||
corrupt_out_of_range (void *closure)
|
||||
{
|
||||
FILE *fp = open_wide_stream ();
|
||||
fp->_wide_data->_wide_vtable_index = IO_VTABLES_NUM + 100;
|
||||
/* Force buffer allocation, which dispatches through the wide vtable
|
||||
(_IO_WDOALLOCATE and friends). */
|
||||
fputwc (L'x', fp);
|
||||
/* Should not be reached. */
|
||||
fclose (fp);
|
||||
}
|
||||
|
||||
/* Callback: a large index, as would result from an attacker overwriting
|
||||
the field with a pointer-sized garbage value truncated into the
|
||||
index. Also out of range, so rejected. */
|
||||
static void
|
||||
corrupt_huge_index (void *closure)
|
||||
{
|
||||
FILE *fp = open_wide_stream ();
|
||||
fp->_wide_data->_wide_vtable_index = 0x41414141;
|
||||
fputwc (L'y', fp);
|
||||
fclose (fp);
|
||||
}
|
||||
|
||||
/* Run CALLBACK in a subprocess and require that it terminates with
|
||||
SIGABRT and prints the fatal stdio message. */
|
||||
static void
|
||||
expect_termination (const char *name, void (*callback) (void *))
|
||||
{
|
||||
struct support_capture_subprocess proc
|
||||
= support_capture_subprocess (callback, NULL);
|
||||
support_capture_subprocess_check (&proc, name, -SIGABRT, sc_allow_stderr);
|
||||
TEST_COMPARE_BLOB (proc.err.buffer, proc.err.length,
|
||||
expected_message, strlen (expected_message));
|
||||
support_capture_subprocess_free (&proc);
|
||||
}
|
||||
|
||||
/* Sanity check: an untampered wide stream works and does not abort. */
|
||||
static void
|
||||
legitimate_stream (void *closure)
|
||||
{
|
||||
FILE *fp = open_wide_stream ();
|
||||
TEST_VERIFY (fputwc (L'z', fp) == L'z');
|
||||
TEST_VERIFY (fclose (fp) == 0);
|
||||
}
|
||||
|
||||
static int
|
||||
do_test (void)
|
||||
{
|
||||
/* The legitimate case must run to completion (exit status 0). */
|
||||
{
|
||||
struct support_capture_subprocess proc
|
||||
= support_capture_subprocess (legitimate_stream, NULL);
|
||||
support_capture_subprocess_check (&proc, "legitimate", 0, sc_allow_stderr);
|
||||
support_capture_subprocess_free (&proc);
|
||||
}
|
||||
|
||||
/* Corruptions must be detected and abort the process. */
|
||||
expect_termination ("out-of-range", corrupt_out_of_range);
|
||||
expect_termination ("huge-index", corrupt_huge_index);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#include <support/test-driver.c>
|
||||
Reference in new issue
Block a user