Skip to content

[RFC] Create a 'safe' strbuf API - #2230

Open
derrickstolee wants to merge 6 commits into
gitgitgadget:ds/trace2-tolerate-failed-timestampfrom
derrickstolee:strbuf-safe
Open

derrickstolee wants to merge 6 commits into
gitgitgadget:ds/trace2-tolerate-failed-timestampfrom
derrickstolee:strbuf-safe

Conversation

@derrickstolee

@derrickstolee derrickstolee commented Sep 17, 2026

Copy link
Copy Markdown

This is based on ds/trace2-tolerate-failed-timestamps [1] [2].

[1] https://lore.kernel.org/git/pull.2178.v3.git.1788197143.gitgitgadget@gmail.com/

[2] #2178

While investigating the fact that the trace2 API can trigger recursive die() loops if allocation fails, Peff pointed out [3] that trace2 uses json-writer which in turn uses the strbuf API. If a strbuf fails to allocate, grow, or otherwise mutate the given strings, then trace2 can hit this problem!

[3] https://lore.kernel.org/git/20260901050129.GB1075462@coredump.intra.peff.net/

The goal of this short RFC, such as it is, is to get some feedback on whether this is a worthwhile direction to pursue or if I should abandon this idea of having this definition of "safe" for some APIs. This decision may also determine if we should abandon ds/trace2-tolerate-failed-timestamps or leave the existing behavior as-is.

I had discussed earlier that what we'd really need is a guarantee that we can't transitively reach die() from any "safe" API. The eventual goal would be to include json-writer.c and the trace2 code files into the "safe" bucket, but for now I'm making sure that strbuf-safe.c satisfies this CodeQL query:

import cpp

class SafeFunction extends Function {
  SafeFunction() {
    getFile().getRelativePath() = "strbuf-safe.c"
  }
}

predicate directlyCalls(Function caller, Function callee) {
  exists(FunctionCall call |
    call.getEnclosingFunction() = caller and
    call.getTarget() = callee
  )
}


from SafeFunction source, Function sink
where
  (sink.getName() = "die" or sink.getName() = "exit") and
  directlyCalls+(source, sink)
select source,
  "This safe function can transitively reach " + sink.getName() + "()."

If we went with this approach, then I'd explore how to make this a build-time requirement during CI.

In regards to the structure of this RFC:

  1. The safe API needs the same structures, but shouldn't import more than necessary. Some movement of structs across headers is done before anything else.
  2. In order to make even the smallest safe method work, we first need to figure out how to handle GIT_ALLOC_LIMIT, which is an undocumented environment variable. I explain that I think this should be GIT_TEST_ALLOC_LIMIT, but maybe the ship has sailed due to Hyrum's Law. So I make an effort to document it but also to initialize it proactively within the process startup instead of implicitly at the lowest level. This allows us to avoid a die() when checking the environment variable.
  3. Thus, we get a 'safe' version of a memory allocation size check. This is our first example of creating a safe version that is then called by the non-safe version to prevent repeated code.
  4. We can then create our first safe strbuf method: sstrbuf_grow(). I explain why I prepend with s instead of appending _gently in the commit.
  5. Some trace2 code implicitly depends on strbuf.h through json-writer.h, so we drop that in favor of strbuf-safe.h to keep the dependence on the full struct definition without forever having the non-safe methods reachable. The goal eventually is to drop the strbuf.h include from json-writer.c, but that isn't accomplished in this RFC.
  6. Finally, create safe init and release methods and use them in json-writer.c. This does show some of the "transition risk" where some json-writer methods become "safe" but I haven't done the hard work to make sure the callers of those methods respond to the new return values. If we proceed with the RFC, then I'd split this into a creation of the safe strbuf methods and then the refactoring required to respond correctly to errors in json-writer.c

Thanks in advance for your thoughts!

Thanks,
-Stolee

cc: gitster@pobox.com
cc: peff@peff.net
cc: newren@gmail.com

The strbuf library is an important API used all over the Git codebase.
Contributors use it in nearly any string-manipulating action. However, the
implementation uses other helping functions that die() on failure instead of
returning an error code. Thus, the strbuf API isn't _safe_.

In particular, we cannot include 'banned-die.h' in 'strbuf.c'.

To start the creation of a safe strbuf API, move the struct definition into
a new 'strbuf-safe.h' header file. All consumers of 'strbuf.h' will consume
that header transitively.

In the future, we will hope to have consumers that need a 'safe' API will
include 'strbuf-safe.h' instead of 'strbuf.h'.

We will see in future changes the inclusion of new implementations that
return an error code instead of halting.

Signed-off-by: Derrick Stolee <stolee@gmail.com>
Before making a safe version of memory_limit_check(), create
initialize_git_alloc_limit() to externalize the static memory limit stored
in that method. Initialize this intentionally during setup_environment()
instead of implicitly during lower-level allocations.

This will allow a future version of memory_limit_check() that doesn't call
die() at all, which will require not calling git_env_ulong() directly. This
comes with some assumption that initialize_git_alloc_limit() is called
before moving into safe APIs, though we will make some reaonable assumptions
in those cases.

The GIT_ALLOC_LIMIT environment variable is used by some tests, but is
otherwise not advertised. It was added by d41489a (Add more large blob
test cases, 2012-03-07), which may predate the GIT_TEST_ pattern. This is
long enough that it may be possible that someone depends on it in the wild.
Thus, I'm choosing to document it instead of renaming it to
GIT_TEST_ALLOC_LIMIT.

Signed-off-by: Derrick Stolee <stolee@gmail.com>
The existing memory_limit_check() is used in many places within wrapper.c,
but because it initializes the GIT_ALLOC_LIMIT environment variable _and_
can call die() when not in gentle mode, this method isn't appropriate for a
safe API.

Modify the implementation to be safe_memory_limit_check() and to keep
calling error() when there is an allocation problem. The original method
calls that version but will die() instead when failing and not gentle.

The one potential behavior change is that when git_alloc_limit is unset we
must assume SIZE_MAX instead of loading the environment variable. Since we
load this environment variable proactively in setup_environment(), this
should only matter for that brief window before setup_environment() and the
safe APIs that call this version. If such safe APIs are used in that window,
then they should allocate small enough amounts of memory to fit under any
reasonable values of GIT_ALLOC_LIMIT.

Signed-off-by: Derrick Stolee <stolee@gmail.com>
After a few changes in preparation, we are now ready to create our first
'safe' strbuf API method: sstrbuf_grow(). This is a safe version of
strbuf_grow().

On naming: For safe equivalents of existing methods, I'm prepending a single
's' character. The intention is to make the safe API non-intrusive.
Alternatives could be to append '_gentle' like many other APIs that avoid a
die() on malformed user data, but we need to be even safer than these gentle
methods, which still die() on allocation failures or other system-level
errors. This 's' prefix is similar to the 'x' prefix used by git-compat-util
helpers.

I selected strbuf_grow() as the first method to move because it doesn't
depend on any other strbuf API method, but is called by many other strbuf
API calls, including strbuf_release() or strbuf_init(). Thus, this will be a
helper to several other implementations that are coming in upcoming changes.

No callers directly depend on sstrbuf_grow(), but the non-safe strbuf_grow()
now uses it as declared in strbuf-safe.h.

Signed-off-by: Derrick Stolee <stolee@gmail.com>
We will use json-writer.c as our first 'safe' API, after removing some
unsafe methods that reach die(), especially the strbuf API. However, the
fact that json-writer.h includes strbuf.h causes someheadaches here.

Normally, we would only declare 'struct strbuf;' as a way to anonymously
define a struct and have implementations include the full header as needed.
However, 'struct json_writer' needs the full struct info and
JSON_WRITER_INIT needs access to STRBUF_INIT.

Thankfully, both are included in strbuf-safe.h, so we can have the header
include the safe API and move an include of strbuf.h to json-writer.c.

However, this has some implications to the trace2 API that includes
json-writer.h and implicitly depends on that includes of strbuf.h. Have
those files include strbuf.h directly to compensate.

Signed-off-by: Derrick Stolee <stolee@gmail.com>
Continue extending the strbuf-safe API by adding these safe versions of the
initialize and release methods:

* sstrbuf_init()
* sstrbuf_release()

These both depend on sstrbuf_grow() that was introduced in the previous
change.

As we are working to make json-writer.c a safe API, adapt its use of
strbuf_release() to the safe version. To properly handle the responses of
the safe versions, some methods are converted to return their own error
codes. However, callers of those methods are not adapted at this time and
will be adapted in future changes. This leaves a window where json-writer
consumers may continue running after an error occurs, potentially leading to
a different error in the future.

Signed-off-by: Derrick Stolee <stolee@gmail.com>
@derrickstolee

Copy link
Copy Markdown
Author

/submit

@gitgitgadget

gitgitgadget Bot commented Sep 18, 2026

Copy link
Copy Markdown

Submitted as pull.2230.git.1789736540.gitgitgadget@gmail.com

To fetch this version into FETCH_HEAD:

git fetch https://github.com/gitgitgadget/git/ pr-2230/derrickstolee/strbuf-safe-v1

To fetch this version to local tag pr-2230/derrickstolee/strbuf-safe-v1:

git fetch --no-tags https://github.com/gitgitgadget/git/ tag pr-2230/derrickstolee/strbuf-safe-v1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant