Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
812f2eb
Add -Werror to setup.py so that all source builds check for warnings.…
juliannguyen4 May 29, 2026
97d3509
Address bug with read_info. TODO write_info
juliannguyen4 May 21, 2026
e968c1c
Fix usage of CPython API
juliannguyen4 May 21, 2026
ef0842e
Finish fixing both read_info and write_info. TODO memory leaks need t…
juliannguyen4 May 21, 2026
657cbfd
Do more refactoring to make helper functions consistent with convert_…
juliannguyen4 May 21, 2026
3e97224
fix compiler error
juliannguyen4 May 21, 2026
dd0d546
Fix syntax err
juliannguyen4 May 21, 2026
ccfe064
Fix tests not following the api docs
juliannguyen4 May 21, 2026
66ae7a3
We can remove this declaration in header if nowhere else is using it.
juliannguyen4 May 26, 2026
51ad3e7
Improve var naming
juliannguyen4 May 26, 2026
5a68322
Fix error message
juliannguyen4 May 26, 2026
896d3b8
Make helper function naming more accurate.
juliannguyen4 May 26, 2026
b4194d6
Add missing as_error_update to convert_nullable_array_of_uint32_to_py…
juliannguyen4 May 26, 2026
11c7e34
Fix c syntax. This happened because my compiler path became unset in …
juliannguyen4 May 28, 2026
625b4ed
Create a helper function that converts an array of any type to a Pyth…
juliannguyen4 May 28, 2026
09af384
Fix improper usage of Py_BuildValue
juliannguyen4 May 28, 2026
a2cf9e3
format_str should be writable..
juliannguyen4 May 28, 2026
f242f2d
Just verified that client fails to compile in macOS with -Werror. Onl…
juliannguyen4 May 29, 2026
9daaac0
Mark as TODO so this doesn't get lost
juliannguyen4 May 29, 2026
9a17540
Merge branch 'CLIENT-4868-cicd-fix-regression-where-smoke-tests-no-lo…
juliannguyen4 May 29, 2026
52b68ba
Use void pointer parameter to accept an array containing any type. I …
juliannguyen4 May 29, 2026
22d0802
Fix invalid cast.
juliannguyen4 May 29, 2026
8e4deca
Since array is a 2D (flat) array of const chars, fix the logic for it…
juliannguyen4 May 29, 2026
5febb3f
Fix incorrect format specifier. l is for signed long int
juliannguyen4 May 29, 2026
4e7fcdb
Fix invalid cast. unsigned long is 64-bit size in linux.
juliannguyen4 May 29, 2026
2ff81de
Merge remote-tracking branch 'origin/dev' into CLIENT-4845-fix-user-d…
juliannguyen4 Jun 2, 2026
b1d4531
convert_nullable_array_to_py_optional_list is only used in conversion…
juliannguyen4 Jun 2, 2026
806eb5d
Add helpful docstring
juliannguyen4 Jun 2, 2026
0219668
Add comments for clarity. Reorder switch cases in increasing numeric …
juliannguyen4 Jun 2, 2026
f1c02d3
Fix potential reference count leak when conns_in_use is returned in a…
juliannguyen4 Jun 2, 2026
c57b966
use macro to reduce code repetition for error handling
juliannguyen4 Jun 2, 2026
22cace4
Improve var naming a bit.
juliannguyen4 Jun 2, 2026
4d47bbd
use case level scoping to create local vars with the same name across…
juliannguyen4 Jun 2, 2026
8a194d4
Address Windows compiler error by fixing pointer arithmetic syntax.
juliannguyen4 Jun 2, 2026
3b5b012
Promote uint32_t value to become unsigned long to be safe.
juliannguyen4 Jun 3, 2026
dd94e56
Merge remote-tracking branch 'origin/dev' into CLIENT-4845-fix-user-d…
juliannguyen4 Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions src/include/conversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,6 @@ as_status as_udf_file_to_pyobject(as_error *err, as_udf_file *entry,
as_status as_udf_files_to_pyobject(as_error *err, as_udf_files *files,
PyObject **py_files);

as_status str_array_of_roles_to_py_list(as_error *err, int num_elements,
char str_array_ptr[][AS_ROLE_SIZE],
PyObject *py_list);

as_status char_double_ptr_to_py_list(as_error *err, int num_elements,
int element_size, char **str_array_ptr,
PyObject *py_list);
Expand Down
174 changes: 110 additions & 64 deletions src/main/conversions.c
Original file line number Diff line number Diff line change
Expand Up @@ -144,30 +144,6 @@ as_status char_double_ptr_to_py_list(as_error *err, int num_elements,
return err->code;
}

as_status str_array_of_roles_to_py_list(as_error *err, int num_elements,
char str_array_ptr[][AS_ROLE_SIZE],
PyObject *py_list)
{
as_error_reset(err);

char *str;

for (int i = 0; i < num_elements; i++) {
str = str_array_ptr[i];
PyObject *py_str = Py_BuildValue("s", str);
if (py_str == NULL) {
as_error_update(err, AEROSPIKE_ERR_CLIENT,
"Unable to build string value from %s.", str);
break;
}

PyList_Append(py_list, py_str);
Py_DECREF(py_str);
}

return err->code;
}

as_status as_user_info_array_to_pyobject(as_error *err, as_user **users,
PyObject **py_as_users, int users_size)
{
Expand Down Expand Up @@ -415,62 +391,132 @@ as_status as_partitions_status_to_pyobject(
return err->code;
}

// format_specifier: type casts each array element and converts it to the right Python type
// This method makes certain assumptions if format_specifier is for converting a string:
// 1. The array is a 2 dimensional array with the strings allocated in one long buffer.
// 2. Each string is AS_ROLE_SIZE chars long.
// TODO - Just refactor later when this helper function needs to handle more cases.
static inline PyObject *convert_nullable_array_to_py_optional_list(
as_error *err, void *array, int array_size, char format_specifier)
{
if (array == NULL) {
Py_RETURN_NONE;
}

PyObject *py_list = PyList_New(0);
if (!py_list) {
as_error_update(err, AEROSPIKE_ERR_CLIENT,
"Failed to create python list");
goto error;
}

for (int i = 0; i < array_size; i++) {
PyObject *py_element = NULL;
char format_str[2];
sprintf(format_str, "%c", format_specifier);
switch (format_specifier) {
case 'k': {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI picked this up, looks like it is worth using I here.

Py_BuildValue format 'k' doesn't match the uint32_t argument

In convert_nullable_array_to_py_optional_list:

case 'k': {
uint32_t element = ((uint32_t *)array)[i];
py_element = Py_BuildValue(format_str, element); // format_str == "k"
break;
}

read_info and write_info are uint32_t* (confirmed in as_admin.h: uint32_t* read_info;, uint32_t* write_info;), and uint32_t is unsigned
int on all target platforms. But the 'k' format code tells Py_BuildValue to read an unsigned long via va_arg.

This is a varargs type mismatch (undefined behavior):

  • Windows (LLP64): unsigned long is 32-bit → happens to match, fine.
  • Linux/macOS x86-64 (LP64): unsigned long is 64-bit. The 32-bit unsigned int is passed in a register that the compiler typically
    zero-extends, so it usually works by luck — but it's relying on UB.
  • aarch64 (LP64): unsigned long is also 64-bit. The AAPCS64 ABI does not guarantee the upper 32 bits of a narrow variadic slot are
    zeroed, so va_arg(ap, unsigned long) can read garbage in the high word → wrong read_info/write_info integers. They build and ship aarch64
    wheels, so this is a genuine cross-platform correctness risk, not purely theoretical.

Fix: use the 'I' (unsigned int) format code for uint32_t:

case 'I': {
uint32_t element = ((uint32_t *)array)[i];
py_element = Py_BuildValue(format_str, element);
break;
}
(and pass 'I' instead of 'k' from as_user_info_to_pyobject). Alternatively keep 'k' but cast the argument: Py_BuildValue("k", (unsigned
long)element). The 'I' change is cleaner. Note their valgrind/CI run on x86-64 will not catch this because of the incidental
zero-extension.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's safe to use I (unsigned int) as the format specifier since it's only guaranteed to be at least 2 bytes large. In theory if unsigned int was 2 bytes large, I think passing in an unsigned 32-bit integer value to I would cause the value to be truncated or cause undefined behavior.

unsigned long is always at least 32 bits long, so I think the unsigned long cast is not needed; unsigned long can always accomodate a uint32_t value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on the I point. I think the problem is when k causes Py_BuildValue to read 64 bits where element is only a 32 bit unsigned int that isn't promoted because Py_BuildValue uses variadic arguments. I think Py_BuildValue("k", (unsigned long)element); would make this safe. Let me know what you think.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I'll make that change

uint32_t element = ((uint32_t *)array)[i];
py_element = Py_BuildValue(format_str, (unsigned long)element);
break;
}
case 's': {
const char *element = (const char *)array + i * AS_ROLE_SIZE;
py_element = Py_BuildValue(format_str, element);
break;
}
}

if (!py_element) {
as_error_update(err, AEROSPIKE_ERR_CLIENT,
"Unable to get list item at index %" PRIu32, i);
goto CLEANUP_ON_ERROR;
}

int retval = PyList_Append(py_list, py_element);
Py_DECREF(py_element);
if (retval == -1) {
as_error_update(err, AEROSPIKE_ERR_CLIENT,
"Unable to append list item at index %" PRIu32, i);
goto CLEANUP_ON_ERROR;
}
}

return py_list;

CLEANUP_ON_ERROR:
Py_DECREF(py_list);
error:
return NULL;
}

#define USER_DICTIONARY_FAILED_TO_SET "Failed to set %s in user dictionary"

as_status as_user_info_to_pyobject(as_error *err, as_user *user,
PyObject **py_as_user)
PyObject **py_user_dict_ref)
{
as_error_reset(err);

PyObject *py_info = PyDict_New();
PyObject *py_roles = PyList_New(0);
PyObject *py_user_dict = PyDict_New();

str_array_of_roles_to_py_list(err, user->roles_size, user->roles, py_roles);
if (err->code != AEROSPIKE_OK) {
Py_DECREF(py_roles);
Py_DECREF(py_info);
goto END;
PyObject *py_list_of_roles = convert_nullable_array_to_py_optional_list(
err, user->roles, user->roles_size, 's');
if (!py_list_of_roles) {
goto CLEANUP_ON_ERROR;
}

if (PyDict_SetItemString(
py_info, "read_info",
Py_BuildValue("i", (user->read_info ? *(user->read_info) : 0))) ==
-1) {
int retval = PyDict_SetItemString(py_user_dict, "roles", py_list_of_roles);
Py_DECREF(py_list_of_roles);
if (retval == -1) {
as_error_update(err, AEROSPIKE_ERR_CLIENT,
"Failed to set %s in py_info.", "read_info");
Py_DECREF(py_roles);
Py_DECREF(py_info);
goto END;
USER_DICTIONARY_FAILED_TO_SET, "roles");
goto CLEANUP_ON_ERROR;
}
if (PyDict_SetItemString(
py_info, "write_info",
Py_BuildValue("i", (user->write_info ? *(user->write_info) : 0))) ==
-1) {
as_error_update(err, AEROSPIKE_ERR_CLIENT,
"Failed to set %s in py_info.", "write_info");
Py_DECREF(py_roles);
Py_DECREF(py_info);
goto END;

uint32_t *arrays[] = {user->read_info, user->write_info};
const char *array_names[] = {"read_info", "write_info"};
int array_sizes[] = {user->read_info_size, user->write_info_size};

for (unsigned long i = 0; i < sizeof(arrays) / sizeof(arrays[0]); i++) {
PyObject *py_optional_list_of_ints =
convert_nullable_array_to_py_optional_list(err, arrays[i],
array_sizes[i], 'k');
if (!py_optional_list_of_ints) {
goto CLEANUP_ON_ERROR;
}

int retval = PyDict_SetItemString(py_user_dict, array_names[i],
py_optional_list_of_ints);
Py_DECREF(py_optional_list_of_ints);
if (retval == -1) {
as_error_update(err, AEROSPIKE_ERR_CLIENT,
USER_DICTIONARY_FAILED_TO_SET, array_names[i]);
goto CLEANUP_ON_ERROR;
}
}
if (PyDict_SetItemString(py_info, "conns_in_use",
Py_BuildValue("i", user->conns_in_use)) == -1) {

PyObject *py_conns_in_use = Py_BuildValue("i", user->conns_in_use);
if (!py_conns_in_use) {
as_error_update(err, AEROSPIKE_ERR_CLIENT,
"Failed to set %s in py_info.", "conns_in_use");
Py_DECREF(py_roles);
Py_DECREF(py_info);
goto END;
"Failed to convert conns_in_use in user dictionary.");
goto CLEANUP_ON_ERROR;
}
if (PyDict_SetItemString(py_info, "roles", py_roles) == -1) {

retval =
PyDict_SetItemString(py_user_dict, "conns_in_use", py_conns_in_use);
Py_DECREF(py_conns_in_use);
if (retval == -1) {
as_error_update(err, AEROSPIKE_ERR_CLIENT,
"Failed to set %s in py_info.", "roles");
Py_DECREF(py_roles);
Py_DECREF(py_info);
goto END;
USER_DICTIONARY_FAILED_TO_SET, "conns_in_use");
goto CLEANUP_ON_ERROR;
}

Py_DECREF(py_roles);
*py_user_dict_ref = py_user_dict;

*py_as_user = py_info;
CLEANUP_ON_ERROR:
if (err->code != AEROSPIKE_OK) {
Py_DECREF(py_user_dict);
}

END:
return err->code;
}

Expand Down
24 changes: 24 additions & 0 deletions test/new_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,27 @@ def expect_earlier_than_server_version_to_fail(as_connection, request):
else:
# InvalidRequest, BinIncompatibleTypes are exceptions that have been raised
request.cls.expected_context_for_pos_tests = pytest.raises(e.ServerError)

def check_user_dictionary(user: dict):
assert set(user["roles"]) == set([
"read",
"read-write",
"sys-admin"
])


# The user has not read or written anything, so all r/w stats should be 0
# NOTE: we don't test the scenario where read_info / write_info is not 0
# because it takes time and a lot of transactions for the server to actually record non-zero values
dict_keys = [
"read_info",
"write_info"
]
for key in dict_keys:
assert type(user[key]) == list
assert len(user[key]) == 4
for element in user[key]:
assert element == 0

# We assume no clients were logged in as this user
assert user.get("conns_in_use") == 0
15 changes: 3 additions & 12 deletions test/new_tests/test_admin_query_user_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import time
from .test_base_class import TestBaseClass
from aerospike import exception as e
from .conftest import check_user_dictionary

import aerospike

Expand Down Expand Up @@ -59,18 +60,8 @@ def test_query_user_info_with_proper_parameters(self):

time.sleep(2)
user_details = self.client.admin_query_user_info(self.user)
assert user_details.get("roles") == [
"read",
"read-write",
"sys-admin"
]
# The user has not read or written anything, so all r/w stats should be 0
# NOTE: we don't test the scenario where read_info / write_info is not 0
# because it takes time and a lot of transactions for the server to actually record non-zero values
assert user_details.get("read_info") == 0
assert user_details.get("write_info") == 0
# No clients were logged in as this user
assert user_details.get("conns_in_use") == 0

check_user_dictionary(user_details)

def test_query_user_info_with_invalid_timeout_policy_value(self):
policy = {"timeout": 0.1}
Expand Down
6 changes: 2 additions & 4 deletions test/new_tests/test_admin_query_users_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import time
from .test_base_class import TestBaseClass
from aerospike import exception as e
from .conftest import check_user_dictionary

import aerospike

Expand Down Expand Up @@ -56,10 +57,7 @@ def test_query_users_info_with_proper_parameters(self):

# Usage test; doesn't actually test if the server records user data
user_details = user_details.get("example-test")
assert user_details.get("roles") == ["read", "read-write", "sys-admin"]
assert user_details.get("read_info") == 0
assert user_details.get("write_info") == 0
assert user_details.get("conns_in_use") == 0
check_user_dictionary(user_details)

def test_query_users_info_with_invalid_timeout_policy_value(self):

Expand Down
Loading