Bug 17908 - freopen() from shared library coredumps when all symbols are hidden in main program
Summary: freopen() from shared library coredumps when all symbols are hidden in main p...
Status: NEW
Alias: None
Product: glibc
Classification: Unclassified
Component: stdio (show other bugs)
Version: 2.21
: P2 normal
Target Milestone: ---
Assignee: Not yet assigned to anyone
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2015-01-30 14:22 UTC by Nick Alcock
Modified: 2015-02-06 09:34 UTC (History)
1 user (show)

See Also:
Host:
Target:
Build:
Last reconfirmed:
fweimer: security-


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Nick Alcock 2015-01-30 14:22:54 UTC
(This affects all glibc versions I've tested, from 2.12 right up to trunk as of yesterday. Most likely it affects all versions from 2.1 up.)

If you compile a trivial main program:

extern void foo (void);
int main (void)
{
  foo();
  return 0;
}

and link it with a version script that hides all symbols, on a platform with an earliest symbol set older than GLIBC_2.2, such as x86-32 or sparc:

SOMETHING_PRIVATE {
  local:
      *;
};

then you will find that you can cause all sorts of fun by calling freopen() or wide-char functions in the shared library containing foo(). Many wide char operations (e.g. fwprintf()) do nothing rather than what you might expect them to do, but you can cause coredumps as well e.g. with a libfoo.so containing one function reading

#include <stdio.h>
int foo (void)
{
  freopen ("/dev/stdout", "a", stdout);
}

we see a coredump with this backtrace:

#0  0xf7e60146 in _IO_flush_all_lockp () from /lib32/libc.so.6
#1  0xf7e602e5 in _IO_cleanup () from /lib32/libc.so.6
#2  0xf7e1ea8a in __run_exit_handlers () from /lib32/libc.so.6
#3  0xf7e1eadd in exit () from /lib32/libc.so.6
#4  0xf7e04a6d in __libc_start_main () from /lib32/libc.so.6
#5  0x08048401 in _start ()

and valgrind says:

==19151== Invalid read of size 4
==19151==    at 0x40E5146: _IO_flush_all_lockp (in /lib32/libc-2.18.so)
==19151==    by 0x40E52E4: _IO_cleanup (in /lib32/libc-2.18.so)
==19151==    by 0x40A3A89: __run_exit_handlers (in /lib32/libc-2.18.so)
==19151==    by 0x40A3ADC: exit (in /lib32/libc-2.18.so)
==19151==    by 0x4089A6C: (below main) (in /lib32/libc-2.18.so)
==19151==  Address 0x85386ef3 is not stack'd, malloc'd or (recently) free'd
==19151==
==19151==
==19151== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==19151==  Access not within mapped region at address 0x85386EF3
==19151==    at 0x40E5146: _IO_flush_all_lockp (in /lib32/libc-2.18.so)
==19151==    by 0x40E52E4: _IO_cleanup (in /lib32/libc-2.18.so)
==19151==    by 0x40A3A89: __run_exit_handlers (in /lib32/libc-2.18.so)
==19151==    by 0x40A3ADC: exit (in /lib32/libc-2.18.so)
==19151==    by 0x4089A6C: (below main) (in /lib32/libc-2.18.so)

With a libfoo.so that opens a local FILE and then freopen()s it

#include <stdio.h>
#include <stdlib.h>

void foo (void)
{
  FILE *foo;
  foo = fopen("/dev/null", "a");
  freopen("/dev/stdout", "a", foo);
}

we see a quite different coredump:

#0  0xf7f1b569 in _IO_old_file_fopen () from /lib32/libc.so.6
#1  0xf7e55852 in freopen () from /lib32/libc.so.6
#2  0xf7fd6657 in foo () at foo.c:9
#3  0x080484f0 in main () at main-nodyn.c:3

==19068== Invalid read of size 4
==19068==    at 0x41A0569: _IO_file_fopen@GLIBC_2.0 (in /lib32/libc-2.18.so)
==19068==    by 0x40DA851: freopen (in /lib32/libc-2.18.so)
==19068==    by 0x4034656: foo (foo.c:9)
==19068==    by 0x80484EF: main (main-nodyn.c:3)
==19068==  Address 0x24 is not stack'd, malloc'd or (recently) free'd
==19068==
==19068==
==19068== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==19068==  Access not within mapped region at address 0x24
==19068==    at 0x41A0569: _IO_file_fopen@GLIBC_2.0 (in /lib32/libc-2.18.so)
==19068==    by 0x40DA851: freopen (in /lib32/libc-2.18.so)
==19068==    by 0x4034656: foo (foo.c:9)
==19068==    by 0x80484EF: main (main-nodyn.c:3)

I suspect that a large number of other failures are also possible, quite possibly up to and including buffer overflows (though I have not proved it, libio's state is confused enough that it seems to me that it might happen).

The underlying problem is that libio depends on looking up the symbol _IO_stdin_used to tell what version of libio was used by the glibc the main program was linked against, so it can avoid silently changing behaviour when glibc is upgraded; ancient glibc 2.0 had no such symbol, so if it's not found, the assumption is that glibc 2.0 is in use. But when the main program has all its symbols hidden, this fires incorrectly, overwriting a new libio operations vector with an old libio operations vector, following which chaos results.

I don't see how to fix this without breaking compatibility with ancient glibc libio users, or inserting a special case in rtld to prevent STV_HIDDEN on _IO_stdin_used from being respected, which would slow down all other users and thus is presumably unacceptable. We could work around this if we had a single additional obscure internal symbol to work with which dated back to the glibc 2.0 days, and which is not also exported by shared libraries (i.e. another symbol like _IO_stdin_used): we could use the visibility of that symbol to tell if _IO_stdin_used was invisible because it was never there, or because it was hidden. But there is no such symbol, as far as I can tell.


It is arguable whether this is even worth fixing. Should we cater for people hiding symbols in the range reserved for the implementation? As far as I can see, though, we have little choice. ld's own manual documents 'local: *' as a legitimate use of version scripts, without caveats. A version script that localized [^_]* and _[_A-Z]* rather than straight * might suffice, but we can't expect users to be that pedantic: more importantly, they haven't been in the past. (I can find no examples of such hyper-pedantic version scripts anywhere.)

The question also arises how common it is to apply a version script containig local: * to a main program, as opposed to a library, at all: perhaps this is so uncommon a problem that it can be ignored. Unfortunately it is not unheard of. On my system, at least LLVM, Mono, and parts of bluez do it: some versions of the JVM reportedly do too. These presumably don't call freopen(), or don't do it often, or they'd be bitten.

It would be interesting to know how many other examples exist on others' systems. Something like

eu-readelf -s /bin/* /usr/bin/* /sbin/* /usr/sbin/* 2>/dev/null | grep -v '^Symbol table' | grep -E ':$|LOCAL.*_IO_stdin_used' | awk 'BEGIN { last_colon=""; } /:$/ { last_colon=$0; next; } /_IO_stdin_used/ { print last_colon; print $0; }'

might give us a clue.