Skip to content

Asynchronous ObtainFile Task Heap Use-After-Free via Stale MmsServerConnection Pointer After Disconnect #597

Description

@gff-cw

Summary

A heap use-after-free exists in the MMS file service implementation of libIEC61850 in the v1.6_develop HEAD commit 196bb76.

The bug is triggered when an ObtainFile / set-file transfer is still in progress and the peer disconnects unexpectedly. The server-side asynchronous upload task (MmsObtainFileTask) keeps a stale raw pointer to the per-connection object (MmsServerConnection). During disconnect handling, the task is only moved to MMS_FILE_UPLOAD_STATE_INTERRUPTED, but its connection field is not cleared. The connection object is then freed by MmsServerConnection_destroy. In the next background-task iteration, mmsServer_fileUploadTask dereferences task->connection->isoConnection before the interrupted-state cleanup path runs, causing a heap-use-after-free.

This issue is reproducible with the official examples:

  • server: examples/server_example_files/server_example_files.c
  • client: examples/iec61850_client_example_files/file-tool.c

The crash occurs in the library code under src/, not in example-specific logic.


Version

Validated commit: 196bb76db9a560b92fc12284c10da7720920b164
Version: 1.6.1+


Full ASan Outcome

Click here for full ASan
=================================================================
==137324==ERROR: AddressSanitizer: heap-use-after-free on address 0x61100000ff58 at pc 0x5a794583e763 bp 0x7e94ffbfe2f0 sp 0x7e94ffbfe2e8
READ of size 8 at 0x61100000ff58 thread T1
    #0 0x5a794583e762 in mmsServer_fileUploadTask /home/weichuan/wc/libiec61850/src/mms/iso_mms/server/mms_file_service.c:487:53
    #1 0x5a79456c9659 in MmsServer_handleBackgroundTasks /home/weichuan/wc/libiec61850/src/mms/iso_mms/server/mms_server.c:874:13
    #2 0x5a79457295d6 in processPeriodicTasks /home/weichuan/wc/libiec61850/src/iec61850/server/mms_mapping/mms_mapping.c:4987:9
    #3 0x5a79457293a3 in IedServer_performPeriodicTasks /home/weichuan/wc/libiec61850/src/iec61850/server/mms_mapping/mms_mapping.c:4993:5
    #4 0x5a79456eea8b in singleThreadedServerThread /home/weichuan/wc/libiec61850/src/iec61850/server/impl/ied_server.c:862:9
    #5 0x7e9502294ac2 in start_thread nptl/./nptl/pthread_create.c:442:8
    #6 0x7e95023268cf  misc/../sysdeps/unix/sysv/linux/x86_64/clone3.S:81

0x61100000ff58 is located 24 bytes inside of 224-byte region [0x61100000ff40,0x611000010020)
freed by thread T1 here:
    #0 0x5a7945676fd2 in free (/home/weichuan/wc/libiec61850/build-asan/examples/server_example_files/server_example_files+0x229fd2) (BuildId: 36ffb47aac6574d8ad306a00a8bbf6158c175554)
    #1 0x5a79458e8a18 in Memory_free /home/weichuan/wc/libiec61850/hal/memory/lib_memory.c:66:5
    #2 0x5a79456ce655 in MmsServerConnection_destroy /home/weichuan/wc/libiec61850/src/mms/iso_mms/server/mms_server_connection.c:848:5
    #3 0x5a79456c90e0 in isoConnectionIndicationHandler /home/weichuan/wc/libiec61850/src/mms/iso_mms/server/mms_server.c:718:13
    #4 0x5a79457dd1f0 in removeTerminatedConnections /home/weichuan/wc/libiec61850/src/mms/iso_server/iso_server.c:277:21
    #5 0x5a79457dd1f0 in handleClientConnections /home/weichuan/wc/libiec61850/src/mms/iso_server/iso_server.c:463:5
    #6 0x5a79457dbb30 in handleIsoConnections /home/weichuan/wc/libiec61850/src/mms/iso_server/iso_server.c
    #7 0x5a79457dbb30 in IsoServer_processIncomingMessages /home/weichuan/wc/libiec61850/src/mms/iso_server/iso_server.c:867:9
    #8 0x5a79456c957f in MmsServer_handleIncomingMessages /home/weichuan/wc/libiec61850/src/mms/iso_mms/server/mms_server.c:846:13
    #9 0x5a79456eea83 in singleThreadedServerThread /home/weichuan/wc/libiec61850/src/iec61850/server/impl/ied_server.c:860:9
    #10 0x7e9502294ac2 in start_thread nptl/./nptl/pthread_create.c:442:8

previously allocated by thread T1 here:
    #0 0x5a7945677468 in __interceptor_calloc (/home/weichuan/wc/libiec61850/build-asan/examples/server_example_files/server_example_files+0x22a468) (BuildId: 36ffb47aac6574d8ad306a00a8bbf6158c175554)
    #1 0x5a79458e899a in Memory_calloc /home/weichuan/wc/libiec61850/hal/memory/lib_memory.c:44:20
    #2 0x5a79456ca5e1 in MmsServerConnection_init /home/weichuan/wc/libiec61850/src/mms/iso_mms/server/mms_server_connection.c:802:38
    #3 0x5a79456c8e44 in isoConnectionIndicationHandler /home/weichuan/wc/libiec61850/src/mms/iso_mms/server/mms_server.c:687:38
    #4 0x5a79457dbb28 in handleIsoConnections /home/weichuan/wc/libiec61850/src/mms/iso_server/iso_server.c:571:13
    #5 0x5a79457dbb28 in IsoServer_processIncomingMessages /home/weichuan/wc/libiec61850/src/mms/iso_server/iso_server.c:867:9
    #6 0x5a79456c957f in MmsServer_handleIncomingMessages /home/weichuan/wc/libiec61850/src/mms/iso_mms/server/mms_server.c:846:13
    #7 0x5a79456eea83 in singleThreadedServerThread /home/weichuan/wc/libiec61850/src/iec61850/server/impl/ied_server.c:860:9
    #8 0x7e9502294ac2 in start_thread nptl/./nptl/pthread_create.c:442:8

Thread T1 created by T0 here:
    #0 0x5a79456606fc in pthread_create (/home/weichuan/wc/libiec61850/build-asan/examples/server_example_files/server_example_files+0x2136fc) (BuildId: 36ffb47aac6574d8ad306a00a8bbf6158c175554)
    #1 0x5a79458e77cc in Thread_start /home/weichuan/wc/libiec61850/hal/thread/linux/thread_linux.c:89:9
    #2 0x5a79456ee815 in IedServer_start /home/weichuan/wc/libiec61850/src/iec61850/server/impl/ied_server.c:886:9
    #3 0x5a79456b20d7 in main /home/weichuan/wc/libiec61850/examples/server_example_files/server_example_files.c:83:5
    #4 0x7e9502229d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: heap-use-after-free /home/weichuan/wc/libiec61850/src/mms/iso_mms/server/mms_file_service.c:487:53 in mmsServer_fileUploadTask
Shadow bytes around the buggy address:
  0x0c227fff9f90: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c227fff9fa0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c227fff9fb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c227fff9fc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c227fff9fd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c227fff9fe0: fa fa fa fa fa fa fa fa fd fd fd[fd]fd fd fd fd
  0x0c227fff9ff0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0c227fffa000: fd fd fd fd fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c227fffa010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c227fffa020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c227fffa030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==137324==ABORTING

Impact

Security impact: remote server-side denial of service via heap use-after-free in the MMS file-service state machine.

An attacker who can reach a deployment exposing the MMS file service path used by ObtainFile / set-file can:

  1. establish a normal MMS connection,
  2. start a large file transfer through the legitimate file-service path,
  3. abort the client process or otherwise force an abnormal disconnect while the upload task is still active.

After disconnect, the server may free the MmsServerConnection object while a background file-upload task still references it. The next periodic background-task pass dereferences the freed object and crashes the server process under ASan, and on non-sanitized builds this can still result in a real crash or other unsafe memory behavior.

At minimum, this is a remote DoS issue in library code. Because the fault is a true heap use-after-free on a connection-associated heap object, the underlying primitive is more severe than a pure logic bug.


Affected Path

Protocol path

MMS file service -> ObtainFile / set-file upload -> disconnect during active transfer -> background task processing

Relevant source files

  • src/mms/inc_private/mms_server_internal.h
  • src/mms/iso_mms/server/mms_file_service.c
  • src/mms/iso_mms/server/mms_server_connection.c
  • src/mms/iso_mms/server/mms_server.c
  • src/iec61850/server/mms_mapping/mms_mapping.c
  • src/iec61850/server/impl/ied_server.c

Observed crash path

mmsServer_fileUploadTask
  -> MmsServer_handleBackgroundTasks
    -> processPeriodicTasks
      -> IedServer_performPeriodicTasks
        -> singleThreadedServerThread

Observed free path

MmsServerConnection_destroy
  -> isoConnectionIndicationHandler
    -> removeTerminatedConnections / handleClientConnections
      -> IsoServer_processIncomingMessages
        -> MmsServer_handleIncomingMessages
          -> singleThreadedServerThread

Root Cause

1. The ObtainFile task stores a raw connection pointer

MmsObtainFileTask keeps a persistent MmsServerConnection pointer inside the task object.
This means the asynchronous file-upload task directly depends on the lifetime of the per-connection heap object.

Conceptually, the task structure contains:

struct sMmsObtainFileTask {
    MmsServerConnection connection;
    ...
    int state;
    ...
};

This is the fundamental ownership problem: the task is longer-lived than a single request, but it only stores a borrowed raw pointer to a connection object that can be destroyed independently.

2. ObtainFile task creation binds the task to the live connection

During ObtainFile handling, the implementation assigns the active connection into the task and transitions the task into the file-upload state machine.

The relevant logic is effectively:

task->connection = connection;
task->obtainFileRequestInvokeId = invokeId;
task->lastRequestInvokeId = MmsServerConnection_getNextRequestInvokeId(connection);
...
task->state = MMS_FILE_UPLOAD_STATE_FILE_OPEN_SENT;

This is the point where the background task becomes tied to the connection object's lifetime.

3. Normal task cleanup clears the connection pointer

The library already contains a proper cleanup routine, changeObtainFileStateToUnused, which fully resets the task and explicitly nulls the pointer:

task->connection = NULL;
...
task->state = MMS_FILE_UPLOAD_STATE_NOT_USED;

This is important because it shows the implementation already recognizes that task->connection must be invalidated when the task is reclaimed.

4. Disconnect cleanup does not clear the pointer

On connection teardown, the helper that stops file-upload tasks only marks matching tasks as interrupted. It does not clear task->connection.

Its effective behavior is:

if (server->fileUploadTasks[i].connection == self) {
    server->fileUploadTasks[i].state = MMS_FILE_UPLOAD_STATE_INTERRUPTED;
}

So after disconnect handling, the task is only half-cleaned:

  • the state changes to INTERRUPTED,
  • but the raw connection pointer still points to the same heap object.

5. The connection object is then freed

MmsServerConnection_destroy invokes the file-upload stop helper and later frees the connection object itself.

So the ordering is:

  1. mark task as interrupted,
  2. keep task->connection unchanged,
  3. free self.

At that point the task contains a dangling pointer.

6. Background task processing dereferences the stale pointer before interrupted-state cleanup

The decisive bug is in mmsServer_fileUploadTask. At function entry it dereferences the connection pointer immediately:

ByteBuffer* message = NULL;
IsoConnection isoConnection = task->connection->isoConnection;

Only after that does it continue toward the task-state handling logic, including the MMS_FILE_UPLOAD_STATE_INTERRUPTED case.

This means the intended interrupted-state cleanup is too late. The code reaches the stale-pointer dereference before it reaches the branch that would reclaim the task.


Reproduction

Build commands

Run in the repository root:

CC=clang CXX=clang++ cmake -S . -B build-asan \
  -DCMAKE_BUILD_TYPE=Debug \
  -DBUILD_EXAMPLES=ON \
  -DBUILD_TESTS=OFF \
  -DCMAKE_C_FLAGS="-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined" \
  -DCMAKE_CXX_FLAGS="-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined" \
  -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" \
  -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address,undefined"

cmake --build build-asan -j"$(nproc)"

Prepare the runtime directories

rm -rf /tmp/libiec61850-uaf
mkdir -p /tmp/libiec61850-uaf/file_server/vmd-filestore
mkdir -p /tmp/libiec61850-uaf/file_client

cp examples/server_example_files/vmd-filestore/test \
  /tmp/libiec61850-uaf/file_server/vmd-filestore/test

printf 'x\n' > /tmp/libiec61850-uaf/file_server/vmd-filestore/IEDSERVER.BIN

cp examples/server_example_files/vmd-filestore/test \
  /tmp/libiec61850-uaf/file_client/test

dd if=/dev/zero of=/tmp/libiec61850-uaf/file_client/big.bin \
  bs=1M count=256

A large file is used so that the upload is still active when the client is killed.

Terminal 1: start the official example server

cd /tmp/libiec61850-uaf/file_server

/.../libiec61850/build-asan/examples/server_example_files/server_example_files 11013

Expected startup banner:

Using libIEC61850 version 1.6.1

Terminal 2: start the official example client and kill it during upload

cd /tmp/libiec61850-uaf/file_client

timeout -s KILL 1.0s \
  /.../libiec61850/build-asan/examples/iec61850_client_example_files/file-tool \
  -h 127.0.0.1 -p 11013 set big.bin

Important: use a relative path (set big.bin) exactly as shown above.

Observed client output

local dir: .
local file: big.bin
filestore basepath: ./
Killed

Observed server output

Using libIEC61850 version 1.6.1
Connection opened
fileAccessHandler: service = 2, local-file: big.bin other-file: big.bin
Connection closed
=================================================================
==137324==ERROR: AddressSanitizer: heap-use-after-free on address 0x61100000ff58
READ of size 8 at 0x61100000ff58 thread T1
    #0  mmsServer_fileUploadTask .../src/mms/iso_mms/server/mms_file_service.c:487:53
    #1  MmsServer_handleBackgroundTasks .../src/mms/iso_mms/server/mms_server.c:874:13
    #2  processPeriodicTasks .../src/iec61850/server/mms_mapping/mms_mapping.c:4987:9
    #3  IedServer_performPeriodicTasks .../src/iec61850/server/mms_mapping/mms_mapping.c:4993:5
    #4  singleThreadedServerThread .../src/iec61850/server/impl/ied_server.c:862:9
...
freed by thread T1 here:
    #2  MmsServerConnection_destroy .../src/mms/iso_mms/server/mms_server_connection.c:848:5
...
previously allocated by thread T1 here:
    #2  MmsServerConnection_init .../src/mms/iso_mms/server/mms_server_connection.c:802:38

Post-crash cleanup before repeating

rm -f /tmp/libiec61850-uaf/file_server/vmd-filestore/big.bin

Then restart the example server and rerun the client command.


Fix Suggestion

A complete fix should address both sides of the lifetime bug:

  1. Disconnect handling must detach the task from the connection immediately.
  2. Background task processing must not dereference task->connection before validating it under the task lock.

Fixing only one side leaves the code fragile.

Recommended remediation strategy

A. Fully detach / reclaim matching file-upload tasks during connection teardown

When a connection is being destroyed, tasks associated with that connection should be reclaimed while holding the task lock, instead of only setting state = INTERRUPTED.

A robust approach is to introduce a helper like:

static void
abortObtainFileTaskOnDisconnect(MmsObtainFileTask task)
{
#if (CONFIG_MMS_THREADLESS_STACK != 1)
    Semaphore_wait(task->taskLock);
#endif

    if (task->fileHandle) {
        FileSystem_closeFile(task->fileHandle);
        task->fileHandle = NULL;
    }

    task->connection = NULL;
    task->frmsId = 0;
    task->destinationFilename[0] = 0;
    task->obtainFileRequestInvokeId = 0;
    task->lastRequestInvokeId = 0;
    task->nextTimeout = 0;
    task->state = MMS_FILE_UPLOAD_STATE_NOT_USED;

#if (CONFIG_MMS_THREADLESS_STACK != 1)
    Semaphore_post(task->taskLock);
#endif
}

Then in the disconnect path:

void
mmsServerConnection_stopFileUploadTasks(MmsServerConnection self)
{
    MmsServer server = self->server;

    for (int i = 0; i < CONFIG_MMS_SERVER_MAX_GET_FILE_TASKS; i++) {
        if (server->fileUploadTasks[i].connection == self) {
            abortObtainFileTaskOnDisconnect(&(server->fileUploadTasks[i]));
        }
    }
}

This is the safest behavior because once the peer is gone, there is no valid target left for the upload task anyway.

B. Guard mmsServer_fileUploadTask against stale / NULL connections before dereference

Even with improved disconnect cleanup, mmsServer_fileUploadTask should not dereference task->connection before validating it.

Suggested restructuring:

void
mmsServer_fileUploadTask(MmsServer self, MmsObtainFileTask task, int taskState)
{
    ByteBuffer* message = NULL;
    IsoConnection isoConnection = NULL;

#if (CONFIG_MMS_THREADLESS_STACK != 1)
    Semaphore_wait(task->taskLock);
#endif

    if (task->state != taskState) {
#if (CONFIG_MMS_THREADLESS_STACK != 1)
        Semaphore_post(task->taskLock);
#endif
        return;
    }

    if (task->connection == NULL) {
        changeObtainFileStateToUnused(task);
#if (CONFIG_MMS_THREADLESS_STACK != 1)
        Semaphore_post(task->taskLock);
#endif
        return;
    }

    isoConnection = task->connection->isoConnection;

#if (CONFIG_MMS_THREADLESS_STACK != 1)
    Semaphore_post(task->taskLock);
#endif

    /* continue with the existing state machine */
}

This prevents the stale-pointer dereference even if some future path forgets to detach the task correctly.

Metadata

Metadata

Assignees

No one assigned

    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