Skip to content

uuid.getnode() is not tied to MAC address when using libuuid #132710

Closed

Description

@sfc-gh-tteixeira

Bug report

Bug description:

According to the docs, uuid.getnode() is meant to return a number based on the MAC address of the network interface. However, if Python is built with libuuid, this does not match the observed behavior. Instead, getnode() often produces a random number.

Versions of Python obtained through python.org, brew, and pyenv don't appear to display this bug. But versions obtained through uv and python-build-standalone do.

The key difference seems to be which branch of this try block inside uuid.py is executed:

# Import optional C extension at toplevel, to help disabling it when testing
try:
    import _uuid
    _generate_time_safe = getattr(_uuid, "generate_time_safe", None)
    _UuidCreate = getattr(_uuid, "UuidCreate", None)
    _has_uuid_generate_time_safe = _uuid.has_uuid_generate_time_safe
except ImportError:
    _uuid = None
    _generate_time_safe = None
    _UuidCreate = None
    _has_uuid_generate_time_safe = None

When the top branch executes, getnode() produces a random number.
When the bottom branch executes, getnode() produces a number tied to the MAC address of the network interface.

Steps to reproduce:

Case 1: working as intended

Using a version of Python compiled with these flags...

        HAVE_UUID_CREATE = "0"
        HAVE_UUID_ENC_BE = "0"
        HAVE_UUID_GENERATE_TIME_SAFE = "1"
        HAVE_UUID_H = "1"
        HAVE_UUID_UUID_H = "1"

...we get this behavior:

$ python -c "import uuid; print(uuid.getnode())"
some number X

$ python -c "import uuid; print(uuid.getnode())"
the same number X

Case 2: buggy behavior

Using a version of Python compiled with these flags...

        HAVE_UUID_CREATE = "0"
        HAVE_UUID_ENC_BE = "0"
        HAVE_UUID_GENERATE_TIME_SAFE = "0"
        HAVE_UUID_H = "0"
        HAVE_UUID_UUID_H = "1"

...we get this behavior:

$ python -c "import uuid; print(uuid.getnode())"
some number X

$ python -c "import uuid; print(uuid.getnode())"
some other number Y!!!

CPython versions tested on:

3.13

Operating systems tested on:

macOS, Linux

Linked PRs

Activity

  1. sfc-gh-tteixeira commented on Apr 18, 2025

    @sfc-gh-tteixeira
    Author

    One thing that caught my eye in the uuid module is this line:

    if os.name == 'posix':
        _GETTERS = [_unix_getnode] + _OS_GETTERS

    In binaries where _uuid._generate_time_safe is available, my understanding is that_unix_getnode will always return a random number. And where not available it returns None.

    So shouldn't _unix_getnode be a used as fallback rather than the first getter?

    That is, shouldn't that line be:

    if os.name == 'posix':
        _GETTERS = _OS_GETTERS + [_unix_getnode]

    ?

  2. savannahostrowski commented on Apr 18, 2025

    @savannahostrowski
    Member

    @picnixz would you be the right person to look at this :) I know you've been doing some work in this space

  3. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Apr 18, 2025
  4. picnixz commented on Apr 18, 2025

    @picnixz
    Member

    _unix_getnode is only used to actually get the node. The node is meant to be constant across UUIDs, so what we do is:

    • Create a UUID using C UUID.
    • Extract the node part to remember this as the MAC.
    • Re-use that MAC later.

    So it's correct to first get _unix_getnode as the first getter. Now, if _unix_getnode returns a random number, the issue is either because of how we use libuuid or an issue in libuuid itself.

    I'll have a more in-depth look tomorrow. I'll also try to build locally to see what happens.

  5. zanieb commented on Apr 19, 2025

    @zanieb
    Contributor

    As a note, HAVE_UUID_GENERATE_TIME_SAFE isn't necessarily the reason for the problem. It's presumably the implementation in the release of libuuid which python-build-standalone uses that's problematic. Other builds, e.g., Debian, use a different libuuid implementation in which HAVE_UUID_GENERATE_TIME_SAFE is set but the node is not random. That said, libuuid's documentation does not say the MAC is guaranteed to be available, so it may be questionable to depend on it as the first lookup method without verifying it is indeed static.

  6. picnixz commented on Apr 19, 2025

    @picnixz
    Member

    That said, libuuid's documentation does not say the MAC is guaranteed to be available, so it may be questionable to depend on it as the first lookup method without verifying it is indeed static.

    That's a good thing to check. Let's just generate a bunch of node addresses, and if they change, fallback to something else.

  7. picnixz commented on Apr 19, 2025

    @picnixz
    Member

    I've looked at the implementation of uuid.h that PBS uses:

    /*
     * Get the ethernet hardware address, if we can find it...
     *
     * XXX for a windows version, probably should use GetAdaptersInfo:
     * http://www.codeguru.com/cpp/i-n/network/networkinformation/article.php/c5451
     * commenting out get_node_id just to get gen_uuid to compile under windows
     * is not the right way to go!
     */
    static int get_node_id(unsigned char *node_id)
    {
    #ifdef HAVE_NET_IF_H
            int             sd;
            struct ifreq    ifr, *ifrp;
            struct ifconf   ifc;
            char buf[1024];
            int             n, i;
            unsigned char   *a;
    #ifdef HAVE_NET_IF_DL_H
            struct sockaddr_dl *sdlp;
    #endif
    
    /*
     * BSD 4.4 defines the size of an ifreq to be
     * max(sizeof(ifreq), sizeof(ifreq.ifr_name)+ifreq.ifr_addr.sa_len
     * However, under earlier systems, sa_len isn't present, so the size is
     * just sizeof(struct ifreq)
     */
    #ifdef HAVE_SA_LEN
    #define ifreq_size(i) max(sizeof(struct ifreq),\
         sizeof((i).ifr_name)+(i).ifr_addr.sa_len)
    #else
    #define ifreq_size(i) sizeof(struct ifreq)
    #endif /* HAVE_SA_LEN */
    
            sd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
            if (sd < 0) {
                    return -1;
            }
            memset(buf, 0, sizeof(buf));
            ifc.ifc_len = sizeof(buf);
            ifc.ifc_buf = buf;
            if (ioctl (sd, SIOCGIFCONF, (char *)&ifc) < 0) {
                    close(sd);
                    return -1;
            }
            n = ifc.ifc_len;
            for (i = 0; i < n; i+= ifreq_size(*ifrp) ) {
                    ifrp = (struct ifreq *)((char *) ifc.ifc_buf+i);
                    strncpy(ifr.ifr_name, ifrp->ifr_name, IFNAMSIZ);
    #ifdef SIOCGIFHWADDR
                    if (ioctl(sd, SIOCGIFHWADDR, &ifr) < 0)
                            continue;
                    a = (unsigned char *) &ifr.ifr_hwaddr.sa_data;
    #else
    #ifdef SIOCGENADDR
                    if (ioctl(sd, SIOCGENADDR, &ifr) < 0)
                            continue;
                    a = (unsigned char *) ifr.ifr_enaddr;
    #else
    #ifdef HAVE_NET_IF_DL_H
                    sdlp = (struct sockaddr_dl *) &ifrp->ifr_addr;
                    if ((sdlp->sdl_family != AF_LINK) || (sdlp->sdl_alen != 6))
                            continue;
                    a = (unsigned char *) &sdlp->sdl_data[sdlp->sdl_nlen];
    #else
                    /*
                     * XXX we don't have a way of getting the hardware
                     * address
                     */
                    close(sd);
                    return 0;
    #endif /* HAVE_NET_IF_DL_H */
    #endif /* SIOCGENADDR */
    #endif /* SIOCGIFHWADDR */
                    if (!a[0] && !a[1] && !a[2] && !a[3] && !a[4] && !a[5])
                            continue;
                    if (node_id) {
                            memcpy(node_id, a, 6);
                            close(sd);
                            return 1;
                    }
            }
            close(sd);
    #endif
            return 0;
    }

    The UUID generation is as follows:

    int __uuid_generate_time(uuid_t out, int *num)
    {
            static unsigned char node_id[6];
            static int has_init = 0;
            struct uuid uu;
            uint32_t        clock_mid;
            int ret;
    
            if (!has_init) {
                    if (get_node_id(node_id) <= 0) {
                            random_get_bytes(node_id, 6);
                            /*
                             * Set multicast bit, to prevent conflicts
                             * with IEEE 802 addresses obtained from
                             * network cards
                             */
                            node_id[0] |= 0x01;
                    }
                    has_init = 1;
            }
            ret = get_clock(&clock_mid, &uu.time_low, &uu.clock_seq, num);
            uu.clock_seq |= 0x8000;
            uu.time_mid = (uint16_t) clock_mid;
            uu.time_hi_and_version = ((clock_mid >> 16) & 0x0FFF) | 0x1000;
            memcpy(uu.node, node_id, 6);
            uuid_pack(&uu, out);
            return ret;
    }

    What we call is

    int uuid_generate_time_safe(uuid_t out)
    {
            return uuid_generate_time_generic(out);
    }

    And the generic implementation is:

    static int uuid_generate_time_generic(uuid_t out) {
    #ifdef HAVE_TLS
            ...
    #else
            if (get_uuid_via_daemon(UUIDD_OP_TIME_UUID, out, 0) == 0)
                    return 0;
    #endif
    
            return __uuid_generate_time(out, 0);
    }

    Now what's important is the following:

    if (!has_init) {
            if (get_node_id(node_id) <= 0) {
                    random_get_bytes(node_id, 6);
                    /*
                     * Set multicast bit, to prevent conflicts
                     * with IEEE 802 addresses obtained from
                     * network cards
                     */
                    node_id[0] |= 0x01;
            }
            has_init = 1;
    }

    If the node cannot be generated, a random one is generated. This is the reason (probably) why we have different node values. However, I see some reasons why we're re-doing the init() path.

    • has_init is being modified by different threads.
    • the function is called from different threads.
    • multiple failures that we're not aware of occur on macOS (namely get_node_id doesn't work well).
    • multiple interpreters will have different values (the result is random for different invocations of Python, but not within the same interpreter AFAICT)

    I'd say the variable should have at least be declared as static volatile instead of static (note that this doesn't guarantee atomicity at all).

  8. picnixz commented on Apr 19, 2025

    @picnixz
    Member

    Note: locally on my machine (openSUSE 15.5, ASUS ROG), I don't have different nodes being created.

  9. picnixz commented on Apr 19, 2025

    @picnixz
    Member

    Note 2: actually, I doubt I can do anything on Python's side. The reason is that the result is cached so changes can only occur if we have different interpreters runs and not just multiple function calls. One way to do it is to materialize two different Python interpreters for the sake of checking but I don't know if it's not just slower then...

    EDIT: Oh I may have a small idea... we could create two configure programs and check for the node's output. If it gives two different results, then we can assume that the function is unstable. But this would only work if for non-cross-builds so I don't know if it can work for the python-build-standalone project @zanieb

  10. changed the title [-]uuid.getnode() is not tied to MAC address[/-] [+]uuid.getnode() is not tied to MAC address when using `libuuid`[/+] on Apr 19, 2025
  11. zanieb commented on Apr 20, 2025

    @zanieb
    Contributor

    Note 2: actually, I doubt I can do anything on Python's side. The reason is that the result is cached so changes can only occur if we have different interpreters runs and not just multiple function calls.

    Ah, this stinks. Hm..

    We could create two configure programs and check for the node's output. If it gives two different results, then we can assume that the function is unstable.

    I think this makes sense, but to start we really just need a variable that's not HAVE_UUID_GENERATE_TIME_SAFE which determines if this is the method that should be used? Like HAVE_STABLE_UUID_GET_NODE or something. Then, for cross-builds it can be set manually but in other builds you can detect it as described above?

    Digging into the root cause... I wonder why get_node_id is not returning a node though? I actually don't see where critical values like HAVE_NET_IF_H, HAVE_NET_IF_DL_H, and HAVE_SA_LEN are ever set? I'm poking at a patch for that to see if it resolves the issue. It seems feasible I can backport whatever makes this work in the latest util-linux version.

  12. picnixz commented on Apr 24, 2025

    @picnixz
    Member

    Good news is: I think I found a way to fix the issue and will push the fix by the end of the week

  13. 13 remaining items

  14. picnixz commented on May 26, 2025

    @picnixz
    Member

    Marking as a deferred blocker so that we remember to merge the 3.13 backport after a few beta releases.

  15. added 2 commits that reference this issue on Jul 12, 2025
  16. hugovk commented on Jul 22, 2025

    @hugovk
    Member

    3.14 RC1 note: this has been fixed for 3.14, the deferred-blocker label is for 3.13.

  17. Yhg1s commented on Aug 4, 2025

    @Yhg1s
    Member

    Should this go into 3.13.6 (scheduled for tomorrow)?

  18. picnixz commented on Aug 4, 2025

    @picnixz
    Member

    Mmh... I'm unfortunately not available until tomorrow, so unless I do something this night, we can wait for 3.13.7. As there are some PR dependencies here and there, either someone else does it, or we wait for one release (and I'll merge when I'm back at the end of the week). I should have checked the 3.13 timeline, my bad (it's not a critical bugfix I think, no one has ever complained about this for the past 10 years...)

  19. added 2 commits that reference this issue on Aug 4, 2025
  20. added a commit that references this issue on Aug 5, 2025
  21. added 2 commits that reference this issue on Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

extension-modulesC modules in the Modules dirstdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions