Skip to content

[Bug] dlopen() fails to find already-loaded module — name mismatch between dlmodule_find() and _dlmodule_set_name() #11268

@jingxbw-work

Description

@jingxbw-work

RT-Thread Version

master (latest)

Hardware Type/Architectures

N/A — platform-independent logic bug in components/libc/posix/libdl/

Develop Toolchain

Microsoft VScode

Describe the bug

Bug Description

Summary

When dlopen() is called with a full file path (e.g. "/mnt/sdcard/apps/clock.so"), it passes the full path to dlmodule_find() for lookup. However, during module loading, _dlmodule_set_name() strips the path and extension, storing only the bare filename (e.g. "clock") into module->parent.name. Since dlmodule_find() uses rt_object_find() which matches against object->name, the lookup always fails for previously loaded modules.

This causes:

  1. Module reloaded on every dlopen() call — the nref++ branch is never reached
  2. Memory leak — duplicate module instances accumulate in RAM
  3. dlclose() cannot properly clean up — each dlopen() returns a different handle pointing to a different copy

Root Cause Analysis

Step 1: dlopen() passes full path to dlmodule_find()

File: dlopen.c, line 38-44

void *dlopen(const char *filename, int flags)
{
    // ...
    fullpath = (char *)filename; /* e.g. "/mnt/sdcard/apps/clock.so" */
 
    rt_enter_critical();
    module = dlmodule_find(fullpath);  // <-- passes full path as-is
    if (module != RT_NULL)
    {
        rt_exit_critical();
        module->nref++;                // <-- NEVER reached due to this bug
    }
    else
    {
        rt_exit_critical();
        module = dlmodule_load(fullpath);  // <-- always falls through here
    }
    // ...
}

Step 2: dlmodule_find() does direct name matching with no path processing

File: dlmodule.c, line 1130-1142

struct rt_dlmodule *dlmodule_find(const char *name)
{
    rt_object_t object;
    struct rt_dlmodule *ret = RT_NULL;
 
    object = rt_object_find(name, RT_Object_Class_Module);
    // ↑ tries to match name == "/mnt/sdcard/apps/clock.so"
    //   but no module is registered under that name
 
    if (object) {
        ret = (struct rt_dlmodule *) object;
    }
    return ret;
}

Step 3: _dlmodule_set_name() strips path and extension before storing

File: dlmodule.c, line 74-99

static void _dlmodule_set_name(struct rt_dlmodule *module, const char *path)
{
    // ...
    while (*ptr != '\0')
    {
        if (*ptr == '/')
            first = ptr + 1;   // skip to after last '/'
        if (*ptr == '.')
            end = ptr - 1;     // stop before last '.'
        ptr ++;
    }
    // For path "/mnt/sdcard/apps/clock.so":
    //   → stores "clock" into object->name
}

The Mismatch

Operation Key used Value
dlmodule_find() query full path "/mnt/sdcard/apps/clock.so"
object->name stored by _dlmodule_set_name() stripped name "clock"

These will never match.

Steps to Reproduce

#include <dlfcn.h>
 
/* First dlopen — module loaded, name stored as "clock" */
void *h1 = dlopen("/mnt/sdcard/apps/clock.so", 0);
 
/* Second dlopen — should find existing module and nref++,
   but actually loads a second copy because find fails */
void *h2 = dlopen("/mnt/sdcard/apps/clock.so", 0);
 
/* h1 != h2 — two independent copies of clock.so now in memory */

Expected Behavior

The second dlopen() call should find the already-loaded module, increment nref, and return the same module handle.

Actual Behavior

Every dlopen() call with a full path loads a new copy of the module. The nref++ reuse branch in dlopen() is effectively dead code — nref is only ever incremented to 1 inside dlmodule_load() itself (line 753), but never reaches 2+ through the intended dlopen() reuse path.

Suggested Fix

Apply the same path-stripping logic before calling dlmodule_find() in dlopen(). Minimal patch:

/* dlopen.c — extract module name from path before find */
void *dlopen(const char *filename, int flags)
{
    struct rt_dlmodule *module;
    char module_name[RT_NAME_MAX];
 
    RT_ASSERT(filename != RT_NULL);
 
    /* Strip path and extension — same logic as _dlmodule_set_name() */
    {
        const char *first, *end, *ptr;
        int size;
 
        ptr = first = filename;
        end = filename + rt_strlen(filename);
 
        while (*ptr != '\0')
        {
            if (*ptr == '/')
                first = ptr + 1;
            if (*ptr == '.')
                end = ptr - 1;
            ptr++;
        }
        size = end - first + 1;
        if (size > RT_NAME_MAX) size = RT_NAME_MAX;
        rt_memset(module_name, 0x00, sizeof(module_name));
        rt_strncpy(module_name, first, size);
        module_name[RT_NAME_MAX - 1] = '\0';
    }
 
    rt_enter_critical();
    module = dlmodule_find(module_name);  /* now matches object->name */
 
    if (module != RT_NULL)
    {
        rt_exit_critical();
        module->nref++;
    }
    else
    {
        rt_exit_critical();
        module = dlmodule_load(filename);  /* still pass full path for file I/O */
    }
 
    return (void *)module;
}

Alternatively, the stripping logic could be moved into dlmodule_find() itself, but that may affect other callers (e.g. dlrun() which already passes a bare module name).

Additional Notes

  1. Dead code on dlopen.c line 28: The condition if (0) //filename[0] != '/' permanently disables the relative-path handling branch. This appears to be a debugging leftover.

  2. Secondary bug in _dlmodule_set_name(): The function finds the last . in the entire path string, not just in the filename portion. For paths like /mnt/v1.2/app.so, end would point into the directory component (v1 instead of app), producing an incorrect module name.

Other additional context

No response

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions