Skip to content

Match uuid.getnode() native node detection to CPython - #8612

Merged
youknowone merged 2 commits into
RustPython:mainfrom
moreal:uuid-getnode-cpython
Aug 30, 2026
Merged

Match uuid.getnode() native node detection to CPython#8612
youknowone merged 2 commits into
RustPython:mainfrom
moreal:uuid-getnode-cpython

Conversation

@moreal

@moreal moreal commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Cache the hardware node used by the native _uuid generator and expose has_stable_extractable_node as an integer capability flag when a MAC address is available.

This allows uuid.getnode() to prefer _unix_getnode() when RustPython's native UUID implementation has a stable extractable node. If hardware lookup fails, RustPython still advertises no stable node and uuid.getnode() keeps its existing platform-command and random fallbacks.

When several MAC addresses are available, the native lookup now prefers a universally administered address and falls back to the first locally administered address, matching the documented uuid.getnode() selection rule.

Python documentation contract

The Python 3.14 documentation for uuid.getnode() defines these observable requirements:

  • return the hardware address as a positive 48-bit integer;
  • if every hardware-address lookup fails, return a random 48-bit number with the multicast bit set;
  • if multiple interfaces exist, prefer universally administered MAC addresses over locally administered MAC addresses, with no other ordering guarantee.

It also notes that the first call may launch a separate program and therefore may be slow. CPython's current implementation caches the first valid result, but the documentation does not separately promise cache identity as part of the public contract.

CPython implementation

RustPython's current Lib/uuid.py was updated from CPython revision 06f9c8ca1cb9abe92507108a299a65ee868d07e3.

CPython gates the native Unix getter on the extension's stable-node capability (Lib/uuid.py permalink):

if _generate_time_safe and _has_stable_extractable_node:
    uuid_time, _ = _generate_time_safe()
    return UUID(bytes=uuid_time).node

getnode() tries that native getter first on POSIX, validates the 48-bit result, caches it, and eventually falls back to a random node (Lib/uuid.py permalink):

global _node
if _node is not None:
    return _node

for getter in _GETTERS + [_random_getnode]:
    try:
        _node = getter()
    except:
        continue
    if (_node is not None) and (0 <= _node < (1 << 48)):
        return _node

Its command-based MAC discovery records the first locally administered address but immediately returns a universally administered address when one is found (Lib/uuid.py permalink):

first_local_mac = None
for line in stdout:
    words = line.lower().rstrip().split()
    for i in range(len(words)):
        if words[i] in keywords:
            try:
                word = words[get_word_index(i)]
                mac = int(word.replace(_MAC_DELIM, b''), 16)
            except (ValueError, IndexError):
                # Virtual interfaces, such as those provided by
                # VPNs, do not have a colon-delimited MAC address
                # as expected, but a 16-byte HWAddr separated by
                # dashes. These should be ignored in favor of a
                # real MAC address
                pass
            else:
                if _is_universal(mac):
                    return mac
                first_local_mac = first_local_mac or mac
return first_local_mac or None

CPython exposes has_stable_extractable_node as an integer and enables it only when the native implementation can provide a stable node (Modules/_uuidmodule.c permalink):

#elif defined(HAVE_UUID_GENERATE_TIME_SAFE_STABLE_MAC)
    ADD_INT("has_stable_extractable_node", 1);
#else
    ADD_INT("has_stable_extractable_node", 0);

The capability value is platform- and build-dependent. CPython's configure probe enables it only when two independent native UUID probes return the same node (configure.ac permalink), and its test repeats _unix_getnode() in two subprocesses and requires equal results (Lib/test/test_uuid.py permalink).

RustPython does not wrap the platform libuuid; its native generator uses rustpython_host_env::socket::mac_address() and falls back to random bytes. This PR caches the MAC lookup and reports the capability as 1 exactly when that lookup succeeds. A missing MAC reports 0, leaving CPython's existing fallback chain active.

Behavior changes

Both CPython's native UUID backend and RustPython's generate_time_safe() may internally use a random node when they cannot obtain a MAC address. In both implementations, a false has_stable_extractable_node capability prevents uuid._unix_getnode() from accepting that native random node, so the public uuid.getnode() continues through the OS-command getters before using _random_getnode(). This PR preserves that fallback behavior; it changes the positive case where RustPython can obtain a stable MAC directly.

The importable _uuid.has_stable_extractable_node attribute is externally observable, although _uuid is an internal, undocumented extension module rather than a public Python API. Its value is platform- and build-dependent, so CPython and RustPython need not report the same number on the same host. The relevant behavior is that it is an integer capability matching whether the native getter can return a stable node. Before this PR RustPython always exposed bool(False) and disabled _unix_getnode(); afterward it exposes int(1) when its MAC lookup succeeds and int(0) otherwise.

The comparison uses one script to format uuid.getnode() as a conventional MAC address, verify caching, and force the documented random fallback:

import _uuid
import re
import subprocess
import sys
import uuid


def mac():
    mac_int = uuid.getnode()
    mac_bytes = [(mac_int >> (5 - i) * 8) & 0xFF for i in range(6)]
    return ":".join(f"{byte:02X}" for byte in mac_bytes)


formatted = mac()
print(
    "capability:",
    type(_uuid.has_stable_extractable_node).__name__,
    _uuid.has_stable_extractable_node,
)
print("native getter available:", uuid._unix_getnode() is not None)
print("getnode MAC:", formatted)
print("MAC format valid:", re.fullmatch(r"(?:[0-9A-F]{2}:){5}[0-9A-F]{2}", formatted) is not None)
print("cached result stable:", formatted == mac())


def nodes_from_fresh_processes(code):
    return [
        int(subprocess.check_output([sys.executable, "-I", "-c", code]))
        for _ in range(3)
    ]


fresh_nodes = nodes_from_fresh_processes("import uuid; print(uuid.getnode())")
print("fresh-process result stable:", len(set(fresh_nodes)) == 1)
print("fresh-process result matches:", all(node == uuid.getnode() for node in fresh_nodes))

uuid._node = None
uuid._GETTERS = [lambda: None]
fallback = uuid.getnode()
print("random fallback valid:", 0 < fallback < (1 << 48))
print("random fallback multicast:", bool(fallback & (1 << 40)))

random_nodes = nodes_from_fresh_processes(
    "import uuid; uuid._GETTERS = [lambda: None]; print(uuid.getnode())"
)
print("fresh random fallback changes:", len(set(random_nodes)) > 1)

The real MAC is redacted below to avoid publishing a machine identifier. The three redacted runtime values were equal when run on the same host. Each fresh-process check also launches three independent interpreter processes: normal getnode() remains equal across all three and matches the parent process, while a forced random fallback changes across processes.

The unmodified CPython tests conditionally skip native-node coverage when the extension cannot advertise or return a stable node:

Before this PR, RustPython reported the capability as False (a bool), so those paths were skipped. After this PR, a supported POSIX host with an available MAC reports 1 (an int), and both original CPython tests execute and pass. This is a skippass transition, not an expectedFailureunexpected success transition. No Python source or test marker under Lib/ is changed.

CPython 3.14.7

$ python3 -I /tmp/uuid-getnode-behavior.py
capability: int 0
native getter available: False
getnode MAC: <redacted>
MAC format valid: True
cached result stable: True
fresh-process result stable: True
fresh-process result matches: True
random fallback valid: True
random fallback multicast: True
fresh random fallback changes: True

RustPython before

$ cargo run -- -I /tmp/uuid-getnode-behavior.py
capability: bool False
native getter available: False
getnode MAC: <redacted>
MAC format valid: True
cached result stable: True
fresh-process result stable: True
fresh-process result matches: True
random fallback valid: True
random fallback multicast: True
fresh random fallback changes: True

RustPython after

$ cargo run -- -I /tmp/uuid-getnode-behavior.py
capability: int 1
native getter available: True
getnode MAC: <redacted>
MAC format valid: True
cached result stable: True
fresh-process result stable: True
fresh-process result matches: True
random fallback valid: True
random fallback multicast: True
fresh random fallback changes: True

Summary by CodeRabbit

  • Enhancements
    • Improved hardware address selection by prioritizing universally administered addresses and using locally administered addresses as a fallback.
    • UUID generation now reuses a stable hardware-based node identifier when available, with randomness as a fallback.
    • UUID capability reporting now dynamically reflects whether a stable hardware node identifier is available.

Cache native MAC address detection and advertise an extractable node only when a hardware address is available. This lets uuid.getnode() use the native UUID path while preserving its existing fallbacks.

Assisted-by: Codex:gpt-5.6-sol
@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: a58a6146-61a8-4533-9039-648151134e75

📥 Commits

Reviewing files that changed from the base of the PR and between 1e3573c and f8e95b4.

📒 Files selected for processing (2)
  • crates/host_env/src/socket.rs
  • crates/stdlib/src/uuid.rs

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Changes

MAC-based UUID node identification

Layer / File(s) Summary
MAC address selection
crates/host_env/src/socket.rs
mac_address prefers a non-zero universally administered address and falls back to the first locally administered address. Tests cover selection and zero-address skipping.
Cached UUID node lookup
crates/stdlib/src/uuid.rs
UUID node identification caches the hardware MAC address, falls back to OS randomness when unavailable, and exposes dynamic hardware-node availability.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to f8e95

The change makes native UUID node selection use a cached host MAC when available while preserving existing fallbacks; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: youknowone

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: aligning uuid.getnode() native node detection with CPython.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the z-ca-2026 Tag to track Contribution Academy 2026 label Aug 30, 2026
@moreal moreal changed the title Match uuid.getnode() native node detection to CPython Match uuid.getnode() native node detection to CPython Aug 30, 2026
Match uuid.getnode()'s documented interface-selection rule by scanning available addresses before falling back to a locally administered one. Ignore zero addresses and cover the selection behavior with Rust unit tests.

Assisted-by: Codex:gpt-5.6-sol
@moreal
moreal marked this pull request as ready for review August 30, 2026 07:10

@youknowone youknowone left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍

@youknowone
youknowone merged commit fc0069f into RustPython:main Aug 30, 2026
50 of 52 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

z-ca-2026 Tag to track Contribution Academy 2026

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants