Page MenuHomeFreeBSD

nlist: Handle multiple symbol tables
ClosedPublic

Authored by des on May 16 2026, 11:05 PM.
Tags
None
Referenced Files
Unknown Object (File)
Sat, Jun 20, 3:24 PM
Unknown Object (File)
Sat, Jun 20, 1:21 PM
Unknown Object (File)
Sat, Jun 20, 11:48 AM
Unknown Object (File)
Wed, Jun 17, 2:02 PM
Unknown Object (File)
Sat, Jun 13, 12:20 PM
Unknown Object (File)
Thu, Jun 4, 2:22 PM
Unknown Object (File)
Thu, Jun 4, 2:17 PM
Unknown Object (File)
Thu, Jun 4, 8:39 AM

Details

Summary
  • Instead of looking for and stopping at the first SHT_SYMTAB section, iterate over all SHT_SYMTAB and SHT_DYNSYM sections until we've either found all our symbols or run out.
  • Perform bounds checks on section and string table offsets and sizes before attempting to mmap() the string table.
  • Perform bounds checks on individual symbol table entries before attempting to access the corresponding strings.
  • Stop treating _Foo and Foo as the same symbol.

This unbreaks OpenSSH which uses nlist(3) to verify PKCS#11 providers.

PR: 295336
MFC after: 1 week
Fixes: 77909f597881 ("Initial elf nlist support [...]")
Fixes: 644b4646c7ac ("OpenSSH: Update to 10.1p1")

Diff Detail

Repository
rG FreeBSD src repository
Lint
Lint Skipped
Unit
Tests Skipped
Build Status
Buildable 73189
Build 70072: arc lint + arc unit

Event Timeline

des requested review of this revision.May 16 2026, 11:05 PM

Thank you!
I'm not expert on libc, but C-wise LGTM.

For purposes of ssh, is it ever right to look into the non-dynamic symbols? And if not, should it use dlsym() instead?

In D57034#1306950, @kib wrote:

For purposes of ssh, is it ever right to look into the non-dynamic symbols? And if not, should it use dlsym() instead?

OpenSSH uses nlist(3) to check if a shared object provides a certain symbol before loading it. It could use dlsym(), but that requires loading the object first, which will execute constructors even if the shared object turns out not to be a functioning PKCS#11 provider. I have no idea how this works upstream since OpenBSD's nlist(3) is the same as ours and doesn't work on shared objects.

I just noticed another problem with the original code: if there is no SHT_SYMTAB section at all, it still tries to mmap() offset 0 of length 0, which is nbd on 14 and prior as it just fails with errno == EINVAL, but on 15 and newer it produces an extended error which specifically references mmap(2). This problem does not exist in my version as it only ever tries to mmap(2) an actual section.

For purposes of ssh, is it ever right to look into the non-dynamic symbols? And if not, should it use dlsym() instead?

As DES mentions, after this nlist() call OpenSSH just calls dlsym (lib_contains_symbol checks nlist's return):

if (lib_contains_symbol(provider_id, "C_GetFunctionList") != 0) {
        error("provider %s is not a PKCS11 library", provider_id);
        goto fail;
}
/* open shared pkcs11-library */
if ((handle = dlopen(provider_id, RTLD_NOW)) == NULL) {
        error("dlopen %s failed: %s", provider_id, dlerror());
        goto fail;
}
if ((getfunctionlist = dlsym(handle, "C_GetFunctionList")) == NULL)
        fatal("dlsym(C_GetFunctionList) failed: %s", dlerror());

openssh-portable has a lib_contains_symbol implementation for systems without nlist() that just mmaps the file and searches for the string with memmem. But in any case our nlist implementation and OpenSSH's use of nlist both seem a little janky.

Maybe we could have a dlopen(3) flag that avoids executing constructors and any other processing beyond parsing the library, and another interface to do so? So we'd have something like:

handle = dlopen(provider_id, RTLD_NOW | RTLD_DEFER_INIT /* or whatever */);
getfunctionlist = dlsym(handle, "C_GetFunctionList"));
if (!getgetfunctionlist)
        error();
rtld_deferred_init(handle)

But after the dlopen(RTLD_DEFER_INIT), would the object participate in the symbol resolution? If yes, it is unsafe. If no, it is probably too hard to implement for the effect.

BTW, this nlist() check would fail for a properly stripped objects as well. Sections can be legitimately absent from the objects. Why doing it at all? What do they try to prevent but that check?

lib/libc/gen/nlist.c
259–260
lib/libc/gen/nlist.c
222

What is the purpose of the lseek() call there? Is it to ensure that elf_scan_symtab accesses the backed mapping (I cannot think about any other reason). But then the seek should be done to the end of the mapped region, not the start? And why not simply check the file size instead?

lib/libc/gen/nlist.c
222

We only map the string table, not the symbol table. The seek positions the file in the right place so elf_scan_symtab() can read() the symbol table.

check string table bounds

des marked an inline comment as done.May 18 2026, 2:32 PM

We'll want to update nlist.3 to describe this; I posted D57065 to document the current behaviour.

Why doing it at all? What do they try to prevent but that check?

It came from:

upstream: Ensure FIDO/PKCS11 libraries contain expected symbols

This checks via nlist(3) that candidate provider libraries contain one
of the symbols that we will require prior to dlopen(), which can cause
a number of side effects, including execution of constructors.

Feedback deraadt; ok markus

OpenBSD-Commit-ID: 1508a5fbd74e329e69a55b56c453c292029aefbe

Now presumably if you can cause the application to load a malicious .so you can also just include the symbol it's looking for. But, there could be some existing .so with a buggy constructor that an exploit would make use of.

kib added inline comments.
lib/libc/gen/nlist.c
253

I think a check that st_name fits into strtab would be useful.

259

This is probably a useless and even harmful remnant from the a.out times. At least we should document that nlist() considers SYMBOL equivalent to _SYMBOL.

This revision is now accepted and ready to land.May 18 2026, 11:17 PM

LGTM with an update to the newly added text in nlist.3, perhaps

diff --git a/lib/libc/gen/nlist.3 b/lib/libc/gen/nlist.3
index 9e2aa0d7eb0f..184d26d59294 100644
--- a/lib/libc/gen/nlist.3
+++ b/lib/libc/gen/nlist.3
@@ -45,10 +45,12 @@ Its use is discouraged.
 The
 .Fn nlist
 function
-retrieves name list entries from the
+retrieves name list entries from
 .Xr elf 5
-section with type
+sections with type
 .Dv SHT_SYMTAB
+or
+.Dv SHT_DYNSYM
 in an ELF object (for example, an executable file or shared library).
 The argument
 .Fa \&nl

Probably also Fixes: 77909f597881 ("Initial elf nlist support, ...")

additional bounds checking + man page tweak

This revision now requires review to proceed.May 18 2026, 11:37 PM
des marked 2 inline comments as done.May 18 2026, 11:37 PM
lib/libc/gen/nlist.c
260–262

This looks correct, but potentially very inefficient. Strings in strtab are not zero-terminated, the strnlen() might scan to the end of the table.

lib/libc/gen/nlist.c
260–262

They clearly must be zero-terminated, otherwise this code would not have worked at all. Here, for instance, is the string table from libutil.so.10:

00015180  00 2e 74 65 78 74 00 2e  67 6f 74 00 2e 67 6f 74  |..text..got..got|
00015190  2e 70 6c 74 00 2e 72 65  6c 61 2e 70 6c 74 00 2e  |.plt..rela.plt..|
000151a0  69 6e 69 74 00 2e 62 73  73 00 2e 64 74 6f 72 73  |init..bss..dtors|
000151b0  00 2e 63 74 6f 72 73 00  2e 64 79 6e 73 74 72 00  |..ctors..dynstr.|
000151c0  2e 65 68 5f 66 72 61 6d  65 5f 68 64 72 00 2e 67  |.eh_frame_hdr..g|
000151d0  6e 75 2e 76 65 72 73 69  6f 6e 5f 72 00 2e 64 61  |nu.version_r..da|
000151e0  74 61 2e 72 65 6c 2e 72  6f 00 2e 72 65 6c 61 2e  |ta.rel.ro..rela.|
000151f0  64 79 6e 00 2e 67 6e 75  2e 76 65 72 73 69 6f 6e  |dyn..gnu.version|
00015200  00 2e 64 79 6e 73 79 6d  00 2e 67 6e 75 5f 64 65  |..dynsym..gnu_de|
00015210  62 75 67 6c 69 6e 6b 00  2e 66 69 6e 69 00 2e 67  |buglink..fini..g|
00015220  6e 75 2e 68 61 73 68 00  2e 72 65 6c 72 6f 5f 70  |nu.hash..relro_p|
00015230  61 64 64 69 6e 67 00 2e  6e 6f 74 65 2e 74 61 67  |adding..note.tag|
00015240  00 2e 65 68 5f 66 72 61  6d 65 00 2e 67 6e 75 2e  |..eh_frame..gnu.|
00015250  76 65 72 73 69 6f 6e 5f  64 00 2e 64 79 6e 61 6d  |version_d..dynam|
00015260  69 63 00 2e 73 68 73 74  72 74 61 62 00 2e 72 6f  |ic..shstrtab..ro|
00015270  64 61 74 61 00 2e 64 61  74 61 00 00 6c 69 62 75  |data..data..libu|
00015280  74 69 6c 2e 73 6f 2e 31  30 2e 64 65 62 75 67 00  |til.so.10.debug.|
260–262

s/the string table/one of the string tables/

des marked an inline comment as done.May 18 2026, 11:50 PM

They clearly must be zero-terminated

Yes they're null terminated. The common optimization / special case with ELF string tables is that strings with a common suffix may be combined.

This revision is now accepted and ready to land.May 18 2026, 11:55 PM

Yes they're null terminated. The common optimization / special case with ELF string tables is that strings with a common suffix may be combined.

Right, that's a common optimization for string tables in general (see for instance JVM string pools), it's all the same to us as long as they're terminated.

This revision was automatically updated to reflect the committed changes.