Skip to content

RuntimeError: cannot use string() on NULL when iterating config with valueless (boolean) keys #1456

@vruyr

Description

@vruyr

Description

Iterating over repo.config raises a RuntimeError if the configuration (including any include.path-sourced files) contains a valueless key — a boolean key written without =, e.g.:

[some-section "identifier"]
    booleanflag

Traceback

Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
  File ".../pygit2/config.py", line 65, in __next__
    return self._next_entry()
           ~~~~~~~~~~~~~~~~^^
  File ".../pygit2/config.py", line 72, in _next_entry
    return ConfigEntry._from_c(centry[0], self)
           ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File ".../pygit2/config.py", line 349, in _from_c
    entry.raw_value = entry.raw_value
                      ^^^^^^^^^^^^^^^
  File ".../functools.py", line 1126, in __get__
    val = self.func(instance)
  File ".../pygit2/config.py", line 369, in raw_value
    return ffi.string(self.c_value)
           ~~~~~~~~~~^^^^^^^^^^^^^^
RuntimeError: cannot use string() on <cdata 'char *' NULL>

Root Cause

libgit2 deliberately sets git_config_entry.value = NULL for valueless keys. The entry struct is zero-initialized via git__calloc, and the value field is only populated when a value is actually present (src/libgit2/config_file.c):

entry = git__calloc(1, sizeof(git_config_list_entry));
/* ... */
if (var_value) {
    entry->base.entry.value = git__strdup(var_value);
    GIT_ERROR_CHECK_ALLOC(entry->base.entry.value);
}

libgit2's own code handles the NULL case explicitly (same file):

else if ((!existing->base.entry.value && !value) ||
         (existing->base.entry.value && value &&
          !strcmp(existing->base.entry.value, value)))

So NULL in git_config_entry.value is intentional and documented behavior.

pygit2's _from_c eagerly caches raw_value during iteration (the workaround introduced for #970):

if iterator is not None:
    entry.raw_name = entry.raw_name
    entry.raw_value = entry.raw_value   # ← crashes when c_value is NULL
    entry.level = entry.level

And raw_value has no NULL guard:

@cached_property
def raw_value(self) -> bytes:
    return ffi.string(self.c_value)   # RuntimeError if c_value is NULL

Expected Behavior

Valueless keys should be represented with value = None (or raw_value = None) rather than raising an exception.

Suggested Fix

Add a NULL check in raw_value:

@cached_property
def raw_value(self) -> bytes | None:
    return ffi.string(self.c_value) if self.c_value != ffi.NULL else None

And propagate accordingly in value:

@property
def value(self) -> str | None:
    return self.raw_value.decode('utf-8') if self.raw_value is not None else None

Reproduction

$ mkdir /tmp/testrepo && cd /tmp/testrepo && git init
Initialized empty Git repository in /tmp/testrepo/.git/

$ printf '[mysection]\n\tbooleanflag\n' >> .git/config

$ cat .git/config
[core]
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
[mysection]
        booleanflag

$ uv run --with pygit2==1.19.1 python - <<EOF
import pygit2
repo = pygit2.Repository('/tmp/testrepo')
for entry in repo.config:
    print(entry.name, entry.value)
EOF
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
  File ".../pygit2/config.py", line 65, in __next__
    return self._next_entry()
           ~~~~~~~~~~~~~~~~^^
  File ".../pygit2/config.py", line 72, in _next_entry
    return ConfigEntry._from_c(centry[0], self)
           ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File ".../pygit2/config.py", line 349, in _from_c
    entry.raw_value = entry.raw_value
                      ^^^^^^^^^^^^^^^
  File ".../functools.py", line 1126, in __get__
    val = self.func(instance)
  File ".../pygit2/config.py", line 369, in raw_value
    return ffi.string(self.c_value)
           ~~~~~~~~~~^^^^^^^^^^^^^^
RuntimeError: cannot use string() on <cdata 'char *' NULL>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions