Skip to content

fix memory leak issue - #1179

Open
AKASHHMISHRA1 wants to merge 1 commit into
aerospike:masterfrom
AKASHHMISHRA1:fix/as-bytes-python-missing-break
Open

fix memory leak issue#1179
AKASHHMISHRA1 wants to merge 1 commit into
aerospike:masterfrom
AKASHHMISHRA1:fix/as-bytes-python-missing-break

Conversation

@AKASHHMISHRA1

@AKASHHMISHRA1 AKASHHMISHRA1 commented Jul 31, 2026

Copy link
Copy Markdown

cc: @juliannguyen4 @dwelch-spike

Summary

deserialize_based_on_as_bytes_type() in src/main/serializer.c is missing a break; statement at the end of the AS_BYTES_PYTHON case, causing execution to fall through into the AS_BYTES_BLOB case immediately below it. This results in a permanent, one-object-per-call native memory leak whenever a bin holding a legacy AS_BYTES_PYTHON value is read back and no custom deserializer is registered.

Root cause

This case was rewritten in 13.0.0 to remove the automatic pickle-based deserialization of AS_BYTES_PYTHON values (per the documented breaking change in that release). The rewrite dropped the internal pickle.loads() call and replaced it with a direct PyByteArray_FromStringAndSize() call — correct on its own — but the closing break; that the original, pre-13.0.0 block had (case AS_BYTES_PYTHON: { ... } break;) was not carried over:

case AS_BYTES_PYTHON:;
    uint32_t bval_size = as_bytes_size(bytes);
    PyObject *py_val = PyByteArray_FromStringAndSize(
        (char *)as_bytes_get(bytes), bval_size);
    if (!py_val) { ... }
    *retval = py_val;
    as_error_update(error_p, AEROSPIKE_OK, NULL);
    // <-- no break here
case AS_BYTES_BLOB: {
    ...

Because a C switch only stops at an explicit break, execution continues directly into AS_BYTES_BLOB's handling. When no custom deserializer is registered (the common case), that branch allocates a second, independent PyObject (via PyBytes_FromStringAndSize) from the same underlying bytes and overwrites *retval with it.

The first object — the PyByteArray created for the AS_BYTES_PYTHON case — is never released. Py_DECREF is never called on it, and the only pointer to it (*retval) is clobbered before the caller ever receives it. It becomes permanently unreachable while its refcount remains at 1, so it can never be freed for the lifetime of the process.

This fires on every single deserialization of an AS_BYTES_PYTHON-typed bin with no registered custom deserializer — i.e., on every read of a value written by a pre-13.0.0 client (or a client relying on the legacy Python-object auto-serialization) that hasn't been rewritten in a newer format.

Impact

In a long-running server process reading such bins at any meaningful request rate, this produces steady, unbounded RSS growth proportional to read volume — eventually leading to OOM.

How this was found

While investigating a memory leak in a production service, we bisected across aerospike-client-python versions under sustained load and found a clean, reproducible boundary:

Version Leaks?
9.0.0 No
11.0.1 No
12.0.0 No
13.0.0 Yes
17.0.0 Yes
19.2.2 Yes

All versions ≤ 12.0.0 retain the original, properly break-terminated AS_BYTES_PYTHON case. All versions ≥ 13.0.0 tested contain the fallthrough. The leak rate observed was consistent with one orphaned object per AS_BYTES_PYTHON deserialization, matching this code path exactly.

We confirmed the same code path (missing break) is still present as of the current master / 19.3.0rc1.

Fix

Add the missing break; at the end of the AS_BYTES_PYTHON case, restoring the original control flow: convert to a bytearray, set the output, and stop — without falling into unrelated blob-deserialization handling.

Testing

This is a minimal, single-line change restoring the pre-13.0.0 control flow for this case. No behavioral change is intended for any other bin type (AS_BYTES_BLOB, AS_BYTES_LIST, AS_BYTES_MAP, etc.) — only the accidental fallthrough into AS_BYTES_BLOB from AS_BYTES_PYTHON is removed.

@juliannguyen4

Copy link
Copy Markdown
Collaborator

Hi @AKASHHMISHRA1, I plan to merge your fix using #1180. I will add you as a co-author to give proper credit.

@AKASHHMISHRA1

Copy link
Copy Markdown
Author

Thanks @juliannguyen4

@juliannguyen4

Copy link
Copy Markdown
Collaborator

I'll publish a version in our custom PyPI repo for you to test out. It should be ready by today.

@AKASHHMISHRA1

Copy link
Copy Markdown
Author

I'll publish a version in our custom PyPI repo for you to test out. It should be ready by today.

Thanks @juliannguyen4 , let me know when it’s done

@juliannguyen4

Copy link
Copy Markdown
Collaborator

Here's a build available with the memory leak fix. Please let me know if it works

python3 -m pip install --extra-index-url=https://artifact.aerospike.io/artifactory/api/pypi/database-pypi-preview-public-local/simple aerospike==19.3.0

@AKASHHMISHRA1

Copy link
Copy Markdown
Author

Hi @juliannguyen4 ,I can’t test this change unless it is available on pypi aerospike package. I see that 19.3.0 is not available there yet.

@juliannguyen4

juliannguyen4 commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

That index URL shouldn't require credentials. Does running that command I posted work for you?

Not all the changes for 19.3.0 are staged yet, so I cannot publish this to PyPI yet. (the command I linked is for installing an early preview build)

Note: I may have to override the build at that URL I sent due to technical restraints with our JFrog build system, so I would recommend saving the build locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants