Compare commits

...

2 Commits

Author SHA1 Message Date
Julien Calixte
8e848894fa feat(firmware): add RECLONE_EACH_BOOT toggle to git_sync
Debug/recovery knob (ships false): when true, wipe /spiflash/repo and re-clone
every boot instead of opening the persistent clone. Doubles as the validation
harness for the FAT read-only-delete fix — the wipe deletes libgit2-created
objects, which only succeeds once they are writable.
2026-07-07 09:33:08 +02:00
Julien Calixte
39e1155bf9 fix(libgit2): create objects writable so FAT can delete them
libgit2 creates loose objects and packfiles with mode 0444 (read-only). FATFS
turns that into the AM_RDO attribute and then refuses to f_unlink them (EACCES),
and esp-idf's FATFS VFS chmod cannot clear AM_RDO — so a written object can
never be deleted, blocking re-clone recovery and (later) fetch/repack.

Shim p_open/p_creat in esp_stubs.c to force owner-write into every create mode
(compile posix.c's originals under throwaway names, same mechanism as p_rename),
so nothing is ever created read-only. Immutability is only a safety hint on an
appliance where nothing but libgit2 touches these files.

Non-regressive on hardware: git_push, which links the same component, still
commits + pushes cleanly with the shim. The delete path itself is validated via
git_sync's RECLONE_EACH_BOOT knob (pending a dedicated flash).
2026-07-07 09:33:08 +02:00
3 changed files with 66 additions and 9 deletions

View File

@@ -107,13 +107,18 @@ target_compile_options(${COMPONENT_LIB} PRIVATE
-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).
# posix.c holds three low-level fs primitives we must replace for FATFS; compile
# the originals under throwaway names — scoped to this ONE file so no other TU is
# touched — and provide FATFS-correct versions in esp_stubs.c:
# p_rename — FATFS f_rename can't replace an existing target and FAT has no
# hardlinks, so libgit2's link-then-rename can't overwrite config/refs/HEAD/
# index during its lock→commit. Ours does remove-then-rename.
# p_open / p_creat — libgit2 creates objects/packs mode 0444; FATFS turns that
# into AM_RDO and then won't f_unlink them (and chmod can't clear it), so
# objects become undeletable (breaks re-clone/fetch/repack). Ours force
# owner-write so nothing is ever created read-only.
set_source_files_properties(
"${LG2}/src/util/posix.c"
PROPERTIES COMPILE_DEFINITIONS "p_rename=libgit2_unused_p_rename"
PROPERTIES COMPILE_DEFINITIONS
"p_rename=libgit2_unused_p_rename;p_open=libgit2_unused_p_open;p_creat=libgit2_unused_p_creat"
)

View File

@@ -14,8 +14,17 @@
#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 */
#include <sys/stat.h> /* stat() + S_IWUSR for utimes()/p_open() below */
#include <stdio.h> /* remove(), rename() for the p_rename replacement */
#include <fcntl.h> /* open(), O_* flags for the p_open()/p_creat() shims */
#include <stdarg.h> /* va_list for the variadic p_open() */
#ifndef O_BINARY
#define O_BINARY 0 /* no text/binary distinction on esp-idf */
#endif
#ifndef O_CLOEXEC
#define O_CLOEXEC 0 /* no exec() on esp-idf, so close-on-exec is a no-op */
#endif
/* One implicit root user/group. */
uid_t getuid(void) { return 0; }
@@ -106,3 +115,32 @@ 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;
}
/* libgit2 creates loose objects and packfiles with mode 0444 (read-only) — the
* git convention that objects are immutable. FATFS honours that as the AM_RDO
* attribute and then refuses to f_unlink the file (EACCES), and esp-idf's FATFS
* VFS chmod() can't clear AM_RDO — so a written object can NEVER be deleted,
* which breaks re-clone recovery and (later) fetch/repack. We force owner-write
* into every create mode so libgit2's files stay writable and therefore
* deletable. Immutability is only a safety hint on an appliance where nothing
* but libgit2 touches these files. posix.c's originals are compiled as
* libgit2_unused_p_open/p_creat (see the component CMakeLists), so these are the
* definitions every other TU links against. Mirrors posix.c's p_open/p_creat. */
int p_open(const char *path, int flags, ...)
{
mode_t mode = 0;
if (flags & O_CREAT) {
va_list arg_list;
va_start(arg_list, flags);
mode = (mode_t)va_arg(arg_list, int);
va_end(arg_list);
mode |= S_IWUSR; /* never create read-only → FATFS won't set AM_RDO */
}
return open(path, flags | O_BINARY | O_CLOEXEC, mode);
}
int p_creat(const char *path, mode_t mode)
{
return open(path, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY | O_CLOEXEC,
mode | S_IWUSR);
}

View File

@@ -76,6 +76,15 @@ const REPO_DIR: &str = "/spiflash/repo";
/// The tracked file we append to. Stands in for the editor's note file(s).
const NOTES_FILE: &str = "notes.md";
/// Debug/recovery + the read-only-delete test knob: when true, wipe REPO_DIR and
/// re-clone from scratch every boot instead of opening the existing clone.
/// Deleting the existing clone's objects only succeeds with the esp_stubs
/// p_open/p_creat fix (libgit2 objects are otherwise mode 0444 → AM_RDO →
/// un-deletable on FAT). Ships **false** — the product opens the persistent
/// clone and fast-forwards; flip true to validate the RO-delete fix or to force
/// a clean re-clone.
const RECLONE_EACH_BOOT: bool = false;
/// GitHub's root CAs, embedded so the push can verify the server's TLS chain.
/// Shared with `git_push` (same file). Written to FAT and handed to libgit2 via
/// GIT_OPT_SET_SSL_CERT_LOCATIONS.
@@ -235,6 +244,11 @@ fn publish() -> Result<String> {
/// whether a clone happened. Clone carries the auth + cert callbacks (the remote
/// may be private, and the TLS chain is verified either way).
fn open_or_clone() -> Result<(Repository, bool)> {
if RECLONE_EACH_BOOT && Path::new(REPO_DIR).exists() {
log::warn!("RECLONE_EACH_BOOT — removing {REPO_DIR} to re-clone from scratch");
fs::remove_dir_all(REPO_DIR)
.context("RECLONE_EACH_BOOT wipe — deletes libgit2 objects, needs the RO-delete fix")?;
}
match Repository::open(REPO_DIR) {
Ok(repo) => {
log::info!("opened existing clone at {REPO_DIR}");