Possible change in sysctlbyname() / oldlenp behavior on iOS and iPadOS 27

I am investigating an issue involving sysctlbyname("hw.machine", ...) that became observable after moving to iOS/iPadOS 27.

The affected legacy code is essentially the following:

void getPlatform(unsigned char machine[])
{
    size_t size;

    sysctlbyname("hw.machine", machine, &size, NULL, 0);

    for (int i = 0; i < size; i++)
    {
        if (machine[i] == ',')
        {
            machine[i] = '.';
        }
    }
}

The caller provides a zero-initialized fixed-size buffer:

unsigned char machine[20] = {0};
getPlatform(machine);

I understand that this implementation is incorrect because size is not initialized.

When oldp is non-NULL, oldlenp must provide the available size of the buffer. A correct implementation would therefore initialize it, for example:

void getPlatform(unsigned char *machine, size_t capacity)
{
    size_t size = capacity;

    if (sysctlbyname("hw.machine", machine, &size, NULL, 0) != 0)
        return;

    for (size_t i = 0; i < size; i++)
    {
        if (machine[i] == ',')
            machine[i] = '.';
    }
}

with:

unsigned char machine[20] = {0};
getPlatform(machine, sizeof(machine));

The question is not whether the original implementation is valid. It clearly relies on an uninitialized value and should be corrected.

What I am trying to understand is why the issue became observable specifically on iOS/iPadOS 27, and whether there has been any related implementation or documentation change.

Using LLDB, I inspected the arguments at the entry to:

sysctlbyname("hw.machine", machine, &size, NULL, 0);

Because size is uninitialized, the value referenced by oldlenp varies depending on the contents of the stack location.

For example, I observed a call where:

*oldlenp = 0

The call then returned:

return = -1
errno = 12 (ENOMEM)

and the output buffer remained empty.

In another execution, the same uninitialized stack location happened to contain a very large value. In that case sysctlbyname() succeeded and returned the expected hardware identifier:

iPhone18,2

Adding unrelated code such as printf() can also change whether the original implementation succeeds, which is consistent with the uninitialized value being affected by changes in stack/register layout.

There is also a second issue I would like clarification on regarding the documented behavior of oldlenp.

The current documentation states that when the amount of data is greater than the value supplied through oldlenp, the function updates it to the required size and returns ENOMEM.

It also states:

The function doesn’t modify the value if it’s larger than or equal to the amount of available data.

However, this does not match what I observed at runtime.

For example, in one successful call I observed:

Before sysctlbyname():
*oldlenp = 4301365248

The value was clearly much larger than required.

After the call returned successfully:

return = 0
machine = "iPhone18,2"
*oldlenp = actual returned data length

In other words, oldlenp was modified on a successful call even though the input value was already much larger than the amount of data being returned.

I would appreciate clarification on the following:

  1. Was there any implementation change to sysctlbyname(), sysctl(), or the handling of oldlenp in iOS/iPadOS 27?
  2. Have there been changes in compiler/runtime behavior on iOS/iPadOS 27 that could make this type of existing uninitialized-variable bug surface more consistently?
  3. Is the documented statement that oldlenp is not modified when the supplied value is sufficiently large still accurate for sysctlbyname() on current iOS versions?
  4. Has the documentation or intended contract for oldlenp changed recently?
  5. Have other developers observed ENOMEM from existing sysctlbyname() code after updating to iOS/iPadOS 27?

Again, I understand that the original code is incorrect and should initialize oldlenp before calling sysctlbyname().

The part I am trying to clarify is whether iOS/iPadOS 27 introduced any behavioral change that exposed this latent bug, and whether the currently documented successful-call behavior of oldlenp matches the actual implementation.

Update after some additional research:

I found an older Apple Developer Forums discussion regarding apparently contradictory sysctl documentation. In that thread, Apple DTS points out that the sysctlbyname page under the Kernel documentation describes the kernel KPI, whereas applications using the user-space BSD sysctlbyname() API should refer to the sysctl(3) man page.

The user-space sysctl(3) documentation states that oldlenp is used as the available buffer size on input, and on return contains the amount of data copied.

This actually matches what I observed in LLDB:

Before:
*oldlenp = 4301365248

After:
return = 0
*oldlenp = 11
machine = "iPhone18,2"

So the change from a very large input value to the actual returned length appears to be expected behavior for the user-space API, rather than an iOS 27 behavioral change.

At this point, the remaining question is mainly whether anything changed in iOS/iPadOS 27 (runtime, compiler, stack layout, etc.) that made the pre-existing uninitialized size bug surface more consistently.

I have not yet found any other public reports of an iOS/iPadOS 27 regression specifically involving sysctlbyname("hw.machine") and ENOMEM.

Possible change in sysctlbyname() / oldlenp behavior on iOS and iPadOS 27
 
 
Q