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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions