fix(libgit2): make the loose ODB persist on FATFS via POSIX shims

The loose ODB silently dropped every write on device: utimes() returned 0
unconditionally, so libgit2's freshen probe (git_futils_touch -> p_utimes)
always reported "object exists" and git_odb_write skipped the write entirely
-- blobs/trees/commits never hit disk and write_tree failed with "invalid
object specified". Gate utimes on file existence (present -> 0, absent ->
ENOENT). Also add a remove-then-rename p_rename (FATFS f_rename can't replace
an existing target, no hardlinks), with posix.c's original scoped out in the
component CMakeLists, plus symlink/gai_strerror link stubs git_push pulls in.

p_rename was verified on hardware (cleared the rename error); the utimes fix
is diagnosed from the failure + libgit2 source and build-verified, on-device
confirmation still pending.
This commit is contained in:
Julien Calixte
2026-07-06 00:18:16 +02:00
parent a0e58e029a
commit 8d80168bda
2 changed files with 64 additions and 2 deletions

View File

@@ -106,3 +106,14 @@ target_compile_options(${COMPONENT_LIB} PRIVATE
-Wno-error=incompatible-pointer-types
-Wno-error=implicit-int
)
# FATFS can't do POSIX rename-replace (f_rename fails if the target exists) and
# has no hardlinks, so libgit2's p_rename (link-then-rename) can't overwrite the
# config/refs/HEAD/index files its lock→commit writes depend on. Compile
# posix.c's p_rename under a throwaway name — scoped to this ONE file so no other
# TU is touched — and provide our own remove-then-rename p_rename in esp_stubs.c
# (p_rename is the single atomic-rename path: filebuf/futils/indexer/refdb_fs).
set_source_files_properties(
"${LG2}/src/util/posix.c"
PROPERTIES COMPILE_DEFINITIONS "p_rename=libgit2_unused_p_rename"
)

View File

@@ -14,6 +14,8 @@
#include <pwd.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/stat.h> /* stat() for the existence-gated utimes() below */
#include <stdio.h> /* remove(), rename() for the p_rename replacement */
/* One implicit root user/group. */
uid_t getuid(void) { return 0; }
@@ -48,10 +50,59 @@ ssize_t readlink(const char *path, char *buf, size_t bufsiz)
return -1;
}
/* VFS has no utimes(); accept and ignore (file mtime is cosmetic here). */
/* No symlinks on FAT: creating one is unsupported. libgit2 calls this only in
* its "does this filesystem support symlinks?" probe (fs_path.c) and while
* copying trees (futils.c) — a clean failure is the honest answer, and libgit2
* treats the target as a normal file. (git_push referenced it; git_smoke, which
* only touched the ODB, did not — hence it surfaces now.) */
int symlink(const char *target, const char *linkpath)
{
(void)target;
(void)linkpath;
errno = ENOSYS;
return -1;
}
/* FATFS/VFS can't set file times — but utimes() MUST still fail for a path that
* doesn't exist. libgit2's git_futils_touch() is p_utimes(), and that is how the
* loose ODB's `freshen` probe answers "does this object already exist?":
* git_odb_write() SKIPS the write entirely when freshen succeeds (odb.c:1629).
* A blanket `return 0` made every freshen succeed, so libgit2 believed every
* object was already on disk and silently dropped ALL loose-object writes —
* blobs/trees/commits never persisted, and write_tree then failed with
* "invalid object specified". Gate success on existence: present → 0 (actually
* setting the time is a cosmetic no-op we skip), absent → -1/ENOENT so freshen
* reports "not found" and the real write proceeds. */
int utimes(const char *path, const struct timeval times[2])
{
(void)path;
struct stat st;
(void)times;
if (stat(path, &st) != 0)
return -1; /* stat set errno (ENOENT for a missing object) */
return 0;
}
/* lwip implements getaddrinfo() but not gai_strerror(); libgit2's socket stream
* (streams/socket.c) uses it only to format a connect-error message, so a
* constant string is sufficient. Deliberately no <netdb.h> include: the symbol
* is undefined at link (no macro/decl shadows it), so a plain definition is
* safe, and the ABI (pointer vs. implicit-int return) matches on xtensa. */
const char *gai_strerror(int ecode)
{
(void)ecode;
return "getaddrinfo failure";
}
/* POSIX rename() atomically replaces an existing target; FATFS f_rename does
* NOT (it fails with EEXIST) and FAT has no hardlinks, so libgit2's own
* p_rename (link-then-rename, in posix.c) can't overwrite config/refs/HEAD/index
* during their lock→commit. Provide replace semantics: drop the target, then
* rename. Not crash-atomic (a crash between the two loses `to`), but FAT offers
* no atomic replace — acceptable for the working copy. posix.c's original is
* compiled as libgit2_unused_p_rename (see the component CMakeLists), so this is
* the p_rename every caller links against. */
int p_rename(const char *from, const char *to)
{
(void)remove(to); /* ignore ENOENT when `to` doesn't exist yet */
return rename(from, to) == 0 ? 0 : -1;
}