Skip to content

Recording stops at 2 GB because the binary, not the card, says so - #124

Merged
snokvist merged 1 commit into
OpenIPC:masterfrom
snokvist:fix/recorder-largefile
Sep 6, 2026
Merged

Recording stops at 2 GB because the binary, not the card, says so#124
snokvist merged 1 commit into
OpenIPC:masterfrom
snokvist:fix/recorder-largefile

Conversation

@snokvist

@snokvist snokvist commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes the root cause in #118. Recordings stopped at exactly 2147483647
bytes
with stop_reason: "write_error", on exFAT, which has no such limit.
The ceiling was never in the media — it was in the binary. A 32-bit glibc
build leaves off_t 32-bit and open() without O_LARGEFILE, so the kernel
returns EFBIG for any write crossing 2^31-1 whatever the filesystem allows
(src/star6e_ts_recorder.c:160, src/star6e_recorder.c:176). record.maxMB
above ~2047 was therefore a knob that validated, applied over MUT_RESTART,
and then silently could not work.

This matters for how the setting is framed: an integrator cannot fix a
32-bit off_t by choosing a better filesystem or a bigger card. The segment
size is theirs to choose; the ceiling is ours to remove.

1. Build with -D_FILE_OFFSET_BITS=64

That is the fix (Makefile:62). It removes the ceiling from open() and
equally from the stat()/fstat() calls at src/venc_recordings.c:162,248
and src/venc_httpd.c:268, which return EOVERFLOW for a file over 2 GB —
so fixing only the write would have produced recordings the listing and the
download then hide. Measured on Star6E against a sparse 3 GB file:

32-bit off_t 64-bit off_t
stat() EOVERFLOW — invisible to /api/v1/recordings ok, 3000000001
fstat() EOVERFLOW — undownloadable ok, 3000000001

The flag must reach every translation unit or the link silently mixes two
off_t layouts, so it is set once in COMMON_CFLAGS and mirrored into
HOST_CFLAGS to keep the tested ABI equal to the shipped one. A CFLAGS
change does not invalidate objects
, so this needs a clean build; the flag
was confirmed at the artifact, not the command line — nm -u on
star6e_ts_recorder.o resolves open64.

2. Rotation is bounded by what off_t can reach

check_rotation() now rotates on whichever limit binds first: the
operator's, or the one the binary can actually write. A max_mb above the
ceiling yields more segments instead of a dead recorder. On every shipped
build the ceiling is UINT64_MAX and this is inert — it is insurance
against the flag being lost, not live behaviour.

3. A segment that cannot be cut stops cleanly

A segment can only open on an IRAP, and the IDR ask is bounded by
TS_RECORDER_MAX_IDR_REQUESTS, so a GDR stream can carry one past any
threshold with no cut coming — the clamp in (2) alone is defeatable exactly
in the case that motivated the IDR-request machinery. Rather than walk into
an EFBIG that truncates mid-access-unit, the recorder stops on the frame
boundary with the file intact and reports size_limit.

That truncation is not hypothetical: in the reproduction below, the
unpatched recorder's own counter stopped at 2147464968 while its file is
2147483647. The failing write was partial.

A ceiling below the off_t one — FAT32 stops at 4 GB — cannot be
anticipated by that guard, since it derives from sizeof(off_t). There the
failure lands inside write_all() mid-access-unit, so the write is now rolled
back to segment_bytes, which is already the offset of the last complete
frame and so costs no lseek. The raw recorder always had this; the TS one
did not, which made size_limit claiming an intact file wrong on exactly that
path.

4. EFBIG is no longer reported as write_error

All three recorders name it, with a new stop reason
RECORDER_STOP_SIZE_LIMIT / "size_limit". Nothing failed — the file is
intact and closed on a frame boundary — and a ceiling calls for lowering
record.maxMB, while an I/O error calls for looking at the card. Reporting
both as write_error is what sent #118 to the SD card.

5. The record status keeps its counters after a self-stop

The inactive branch of the status callback filled in only stop_reason, so
a recorder that stopped by itself answered {"path":"","frames":0, "bytes":0,"segments":0} — losing both the file that was cut short and how
far it got, which is the entire diagnosis for a non-manual stop. It now
carries path, bytes, frames and segments of the recording that ended, on all
three backends.

Which recorder that is comes from record.format, not from guessing. An
earlier revision picked "the TS reason unless it is manual", which says
nothing about which session ended last — a retained write_error would have
outranked a later, cleanly stopped recording and, with the counters now
following the reason, named that older file too. record.format is
restart_required, so only one recorder can have run in a given process and
the ambiguity is removed rather than arbitrated.

6. Docs

maxSeconds/maxMB of 0 were documented as "no limit", but the runtime
overrides its compiled-in default only when the value is > 0 — so 0
means 300 s / 500 MB, not unlimited. Corrected, along with a statement that
rotation is format: "ts" only.

Evidence

Three arms of one harness driving the real recorder core on Star6E
(SSC338Q), same 58 GB FAT32 card, same deterministic input, max_mb=2500
throughout — only the build differs.

unpatched (control) patched, no flag patched + flag
sizeof(off_t) 4 4 8
outcome EFBIG "File too large" rotated below the ceiling ran through
bytes written 2147464968, then dead 2306867536 2306867536
segments 1 2 1
largest file 2147483647 2147212672 2306867536
exit 1 0 0

The control arm reproduces #118 byte for byte, so the instrument is known
to detect the defect before either negative is trusted. The two patched arms
wrote identical totals from identical input, so segmentation is the only
variable between them.

CV610 and Maruko are not affected and are not changed by this. musl is
64-bit off_t and forces O_LARGEFILE into every open() with or without
the flag — measured on CV610, not assumed. That negative is only trustworthy
because the same probe's second arm opened the same file through a raw
openat syscall, bypassing the libc wrapper and guaranteed to lack
O_LARGEFILE, and did fail with EFBIG on that kernel. A probe whose
negative arm also passes proves nothing.

Method note: file sizes are read from the filesystem after each run, not from
the recorder's counters — the control arm shows those two disagreeing, so the
counter alone would have hidden the partial write.

Validation

  • make build clean on all three backends from make clean; no new warnings.
  • make test3028 passed, 0 failed. New coverage:
    test_ts_size_limit_rolls_back_partial_write uses RLIMIT_FSIZE to pose a
    below-off_t ceiling, which is what makes this reachable from a host test at
    all — the off_t ceiling itself is unreachable where off_t is 64-bit. It
    asserts the stop is classified size_limit and that the file ends on a whole
    number of TS packets. Mutation-checked both ways: removing the rollback fails
    2 assertions, misclassifying EFBIG fails 1. The repo's own
    doc_app_version_matches_VERSION / doc_contract_version_matches_code gates
    also caught an incomplete version bump mid-change.
  • AGENTS.md:553 (~80-line functions): the ceiling guard and error
    classification moved into ts_write_muxed(), leaving muxing as
    write_video()'s job; it is now 77 lines.
  • Hardware: Star6E (SSC338Q) with a 58 GB FAT32 card for the three arms and
    the stat() probe; CV610 for the O_LARGEFILE probe. The harness drives
    the SoC-independent recorder entry points and links only -lpthread, so it
    ran alongside a live waybeam without disturbing it.
  • No host test added. The new paths are reachable only where off_t is
    32-bit; a host test would have to fake the ceiling to reach them, and one
    that can only pass proves nothing. The 21 existing TS-recorder tests cover
    the rotation logic this touches.

contract_version 0.29.00.30.0 — the stop_reason enum gains a value
and the inactive status payload changes meaning, both non-breaking under the
contract's own governance rules. VERSION 0.82.00.83.0.

Review

Qodo's review found three issues, all real and all fixed in the current head:
the stale-recorder selection above (a bug this PR introduced by making the
counters follow the reason), write_video() exceeding the ~80-line guideline,
and the partial-write truncation above — which this PR's own evidence table had
already recorded as the 18679-byte gap between 2147464968 and 2147483647
without my noticing what it implied.

Not in this change

format: "hevc" ignores maxSeconds/maxMB entirely — the raw recorder has
no rotation code at all. That is why the "lower maxMB" workaround in #118
does not generalise. Filed separately as #123: it is new behaviour in a path
with no segment state, needing its own cut boundary and tests. The docs here
state the current behaviour so it is no longer a doc/code mismatch either way.

🤖 Generated with Claude Code

https://claude.ai/code/session_01U1w7gRc6ASpa7P7HRCBQEw

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Enable large-file recording and report size-limit stops

🐞 Bug fix ✨ Enhancement 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Enable 64-bit file offsets so recordings, listings, and downloads work beyond 2 GB.
• Rotate or stop cleanly at size ceilings and report size_limit instead of write errors.
• Preserve self-stopped recording counters and clarify TS-only rotation defaults.
Diagram

graph TD
  H["64-bit off_t Build"] --> C["Rotation Guard"]
  A["Encoded Frame"] --> B{"Recorder Format"} -->|TS| C -->|IRAP available| D["Rotate Segment"] --> F["Recorder Snapshot"] --> G["Status API"]
  C -->|Ceiling reached| E["Clean Size Stop"] --> F
  B -->|Raw HEVC EFBIG| E
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use explicit large-file APIs
  • ➕ Makes large-file calls explicit at each access site.
  • ➕ Avoids relying on a feature-test macro for individual operations.
  • ➖ Requires consistently replacing open, stat, fstat, and related interfaces.
  • ➖ Missed calls could still hide or prevent downloads of large recordings.
  • ➖ Risks mixing incompatible off_t and struct stat layouts across modules.
2. Always rotate below 2 GB
  • ➕ Avoids the affected 32-bit offset boundary.
  • ➕ Requires no global compilation ABI change.
  • ➖ Preserves the underlying listing and download limitations.
  • ➖ Unnecessarily restricts segment sizes on capable targets.
  • ➖ Does not protect unrotated HEVC recordings or failed IRAP rotation.

Recommendation: Keep the PR's global _FILE_OFFSET_BITS=64 approach. It fixes writing, metadata inspection, and downloads consistently across translation units; the runtime ceiling guard remains appropriate defense-in-depth if the build flag is lost. Explicit *64 APIs are more error-prone, while unconditional sub-2-GB rotation only masks the ABI defect.

Files changed (14) +278 / -28

Bug fix (7) +164 / -13
star6e_recorder.hDefine the writable offset ceiling and size-limit stop reason +19/-0

Define the writable offset ceiling and size-limit stop reason

• Introduces an ABI-derived maximum writable offset for defensive rotation and frame-boundary checks. Adds 'RECORDER_STOP_SIZE_LIMIT' to distinguish file-size ceilings from storage write failures.

include/star6e_recorder.h

cv610_runtime.cPreserve CV610 self-stopped recording status +18/-1

Preserve CV610 self-stopped recording status

• Maps recorder size-limit stops to 'size_limit'. Copies path, byte, frame, and segment values from the same completed-recorder snapshot that supplied the stop reason.

src/cv610_runtime.c

maruko_recorder.cClassify Maruko raw-recorder EFBIG failures as size limits +13/-0

Classify Maruko raw-recorder EFBIG failures as size limits

• Handles 'EFBIG' separately from generic write errors, records the size-limit stop reason, and advises using TS for long rotating recordings.

src/maruko_recorder.c

maruko_runtime.cPreserve Maruko self-stopped recording status +18/-1

Preserve Maruko self-stopped recording status

• Exposes 'size_limit' through record status and retains the completed recorder's path and counters instead of returning empty values.

src/maruko_runtime.c

star6e_recorder.cReport raw recorder file ceilings explicitly +20/-0

Report raw recorder file ceilings explicitly

• Adds size-limit text for stop logging and maps 'EFBIG' from both access-unit and stream write paths to the dedicated stop reason.

src/star6e_recorder.c

star6e_runtime.cPreserve Star6E self-stopped recording status +18/-1

Preserve Star6E self-stopped recording status

• Maps the new stop reason to 'size_limit' and returns the finished recorder's path, bytes, frames, and segment count while inactive.

src/star6e_runtime.c

star6e_ts_recorder.cBound TS rotation and stop safely at file ceilings +58/-10

Bound TS rotation and stop safely at file ceilings

• Clamps segment rotation to the lesser of the configured threshold and writable offset ceiling, reserving one TS buffer of headroom. Stops before crossing the ceiling when no IRAP permits rotation and classifies fallback 'EFBIG' failures as size limits.

src/star6e_ts_recorder.c

Documentation (4) +95 / -11
HISTORY.mdDocument the 0.83.0 large-file recording release +42/-0

Document the 0.83.0 large-file recording release

• Adds release notes covering the 64-bit file-offset fix, bounded rotation, clean size-limit stops, preserved status counters, and corrected rotation documentation.

HISTORY.md

README.mdRefresh documented application and contract versions +1/-1

Refresh documented application and contract versions

• Updates the version endpoint example to application version 0.83.0 and contract version 0.30.0.

README.md

HTTP_API_CONTRACT.mdDefine size-limit stops and retained inactive status data +17/-4

Define size-limit stops and retained inactive status data

• Bumps the documented contract to 0.30.0 and adds 'size_limit' to recording stop reasons. Specifies that self-stopped recordings retain their path and counters while inactive.

documentation/HTTP_API_CONTRACT.md

SD_CARD_RECORDING.mdCorrect rotation defaults and document file-size behavior +35/-6

Correct rotation defaults and document file-size behavior

• Clarifies that zero retains built-in rotation thresholds rather than disabling them, and that rotation applies only to TS. Documents IRAP-bound rotation, binary offset ceilings, 'size_limit', and retained status counters.

documentation/SD_CARD_RECORDING.md

Other (3) +19 / -4
MakefileBuild all target and host code with 64-bit file offsets +17/-2

Build all target and host code with 64-bit file offsets

• Adds '_FILE_OFFSET_BITS=64' to common target flags so writes and metadata operations support files beyond 2 GB. Mirrors the definition in host test flags to keep the tested ABI aligned with shipped binaries.

Makefile

VERSIONBump the application version to 0.83.0 +1/-1

Bump the application version to 0.83.0

• Advances the release version from 0.82.0 to 0.83.0.

VERSION

venc_api.hAdvance the API contract version to 0.30.0 +1/-1

Advance the API contract version to 0.30.0

• Updates the compiled contract version for the added stop reason and changed inactive status semantics.

include/venc_api.h

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (2) 📎 Requirement gaps (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Status selects stale recorder 📘 Rule violation ≡ Correctness
Description
Inactive status chooses between independently retained recorder snapshots using persistent stop
reasons rather than session recency, so a manual TS stop can select an older raw-recorder snapshot
and an old TS failure can override a more recent manually stopped HEVC recording. Failed starts can
also set disk_full before retained metadata is reset, causing all three backends to return stale
paths, counters, and stop reasons for an earlier recording or attribute them to an attempt that
never started.
Code

src/star6e_runtime.c[R836-839]

+			if (sr == RECORDER_STOP_MANUAL) {
				sr = rec_snap.last_stop_reason;
+				last = &rec_snap;
+			}
Evidence
The updated contract requires inactive path, frames, bytes, and segments values to describe
the recording that ended, but both recorder snapshots retain paths, counters, and reasons
independently, and each reason is reset only when that recorder successfully starts. The callbacks
select snapshots based on the TS recorder's historical reason—switching to raw when it is manual
and selecting TS when it is non-manual—even though that value does not establish which session ended
most recently; additionally, preflight failures can set disk_full before old counters and paths
are reset, proving that the selected snapshot may belong to an older recording or an attempt that
never started.

AGENTS.md: Document HTTP API Behavior Changes
documentation/HTTP_API_CONTRACT.md[1285-1290]
src/star6e_runtime.c[833-857]
src/maruko_runtime.c[105-129]
src/cv610_runtime.c[756-780]
src/star6e_recorder.c[189-195]
src/star6e_ts_recorder.c[232-248]
src/star6e_runtime.c[803-861]
src/maruko_runtime.c[102-132]
src/cv610_runtime.c[753-783]
src/star6e_ts_recorder.c[220-260]
src/star6e_recorder.c[157-200]
src/star6e_recorder.c[470-487]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Inactive recording status infers snapshot ownership and recency from persistent `last_stop_reason` values. This can select stale metadata from an older TS or raw/HEVC recording, or expose retained metadata as belonging to a failed start, instead of reporting the recording that most recently ended.

## Issue Context
The API contract requires inactive `path`, `frames`, `bytes`, and `segments` to describe the recording that ended. TS and HEVC/raw recorder state is retained independently, and a manual or non-manual stop reason is not a reliable recency signal; track which recorder owns the current or most recently ended session by adding explicit stop sequencing/generation metadata or maintaining one authoritative last-completed-recording snapshot. Ensure failed starts do not associate old counters and paths with a new stop reason, and apply the correction consistently across all three runtime callbacks.

## Fix Focus Areas
- src/star6e_runtime.c[829-861]
- src/maruko_runtime.c[101-132]
- src/cv610_runtime.c[752-783]
- src/star6e_ts_recorder.c[212-264]
- src/star6e_recorder.c[150-203]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. write_video() exceeds size guideline 📘 Rule violation ⚙ Maintainability
Description
The new ceiling guard and error-classification logic expand star6e_ts_recorder_write_video() to
approximately 115 lines. The function now combines muxing, ceiling enforcement, error reporting,
counter updates, and synchronization instead of delegating focused operations to helpers.
Code

src/star6e_ts_recorder.c[R455-462]

+		if ((uint64_t)ts_len > RECORDER_OFF_T_CEILING - state->segment_bytes) {
+			fprintf(stderr,
+				"[ts_recorder] segment at %llu bytes cannot grow past the "
+				"32-bit file ceiling and the stream produced no IRAP to "
+				"rotate on; stopping\n",
+				(unsigned long long)state->segment_bytes);
+			stop_with_reason(state, RECORDER_STOP_SIZE_LIMIT);
+			return -1;
Evidence
The checklist limits modified functions to approximately 80 lines and requires dense logic to be
decomposed. The modified function spans lines 401–515, with the newly added inline size-limit branch
contributing substantial additional control flow and diagnostics.

AGENTS.md: Keep Functions Short, Clear, and Focused
src/star6e_ts_recorder.c[401-515]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`star6e_ts_recorder_write_video()` substantially exceeds the approximately 80-line function guideline after adding inline ceiling enforcement and `EFBIG` handling.

## Issue Context
Keep frame muxing as the function's main responsibility. Extract the file-ceiling guard and classified write/error handling into focused static helpers without changing rotation or stop-reason behavior.

## Fix Focus Areas
- src/star6e_ts_recorder.c[401-515]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Filesystem limit truncates access unit 🐞 Bug ≡ Correctness
Description
The new EFBIG path labels the TS file intact, but write_all() may already have written part of
the access unit before encountering a filesystem ceiling below the off_t ceiling. Because this
path does not truncate back to the pre-write offset, recordings exceeding limits such as FAT32's can
end with a partial TS access unit while reporting size_limit.
Code

src/star6e_ts_recorder.c[R469-472]

+			} else if (errno == EFBIG) {
+				/* Only reachable if the guard above was defeated --
+				 * a shrinking off_t, or a filesystem cap below it
+				 * (FAT32's 4 GB).  Name it: "write error" sent the
Evidence
The ceiling guard derives exclusively from sizeof(off_t), so it cannot anticipate a lower
filesystem limit. write_all() returns -1 after an error without undoing bytes written by
preceding loop iterations, and the added TS EFBIG branch stops without the ftruncate() rollback
already used by the raw recorder.

include/star6e_recorder.h[20-31]
src/star6e_ts_recorder.c[33-48]
src/star6e_ts_recorder.c[455-484]
src/star6e_recorder.c[265-290]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A TS write can partially succeed before returning `EFBIG` at a filesystem-specific size ceiling. The recorder reports `size_limit` without truncating the partial access unit, contradicting the API guarantee that the file ends on a frame boundary.

## Issue Context
`RECORDER_OFF_T_CEILING` only covers the ABI offset limit and is `UINT64_MAX` in the shipped build. The raw recorder already records the frame offset and calls `ftruncate()` after failed writes; the TS recorder needs equivalent behavior.

## Fix Focus Areas
- src/star6e_ts_recorder.c[444-484]
- src/star6e_recorder.c[251-296]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/star6e_runtime.c Outdated
Comment on lines +836 to +839
if (sr == RECORDER_STOP_MANUAL) {
sr = rec_snap.last_stop_reason;
last = &rec_snap;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Status selects stale recorder 📘 Rule violation ≡ Correctness

Inactive status chooses between independently retained recorder snapshots using persistent stop
reasons rather than session recency, so a manual TS stop can select an older raw-recorder snapshot
and an old TS failure can override a more recent manually stopped HEVC recording. Failed starts can
also set disk_full before retained metadata is reset, causing all three backends to return stale
paths, counters, and stop reasons for an earlier recording or attribute them to an attempt that
never started.
Agent Prompt
## Issue description
Inactive recording status infers snapshot ownership and recency from persistent `last_stop_reason` values. This can select stale metadata from an older TS or raw/HEVC recording, or expose retained metadata as belonging to a failed start, instead of reporting the recording that most recently ended.

## Issue Context
The API contract requires inactive `path`, `frames`, `bytes`, and `segments` to describe the recording that ended. TS and HEVC/raw recorder state is retained independently, and a manual or non-manual stop reason is not a reliable recency signal; track which recorder owns the current or most recently ended session by adding explicit stop sequencing/generation metadata or maintaining one authoritative last-completed-recording snapshot. Ensure failed starts do not associate old counters and paths with a new stop reason, and apply the correction consistently across all three runtime callbacks.

## Fix Focus Areas
- src/star6e_runtime.c[829-861]
- src/maruko_runtime.c[101-132]
- src/cv610_runtime.c[752-783]
- src/star6e_ts_recorder.c[212-264]
- src/star6e_recorder.c[150-203]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/star6e_ts_recorder.c Outdated
Comment on lines +455 to +462
if ((uint64_t)ts_len > RECORDER_OFF_T_CEILING - state->segment_bytes) {
fprintf(stderr,
"[ts_recorder] segment at %llu bytes cannot grow past the "
"32-bit file ceiling and the stream produced no IRAP to "
"rotate on; stopping\n",
(unsigned long long)state->segment_bytes);
stop_with_reason(state, RECORDER_STOP_SIZE_LIMIT);
return -1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. write_video() exceeds size guideline 📘 Rule violation ⚙ Maintainability

The new ceiling guard and error-classification logic expand star6e_ts_recorder_write_video() to
approximately 115 lines. The function now combines muxing, ceiling enforcement, error reporting,
counter updates, and synchronization instead of delegating focused operations to helpers.
Agent Prompt
## Issue description
`star6e_ts_recorder_write_video()` substantially exceeds the approximately 80-line function guideline after adding inline ceiling enforcement and `EFBIG` handling.

## Issue Context
Keep frame muxing as the function's main responsibility. Extract the file-ceiling guard and classified write/error handling into focused static helpers without changing rotation or stop-reason behavior.

## Fix Focus Areas
- src/star6e_ts_recorder.c[401-515]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/star6e_ts_recorder.c Outdated
Comment on lines +469 to +472
} else if (errno == EFBIG) {
/* Only reachable if the guard above was defeated --
* a shrinking off_t, or a filesystem cap below it
* (FAT32's 4 GB). Name it: "write error" sent the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Filesystem limit truncates access unit 🐞 Bug ≡ Correctness

The new EFBIG path labels the TS file intact, but write_all() may already have written part of
the access unit before encountering a filesystem ceiling below the off_t ceiling. Because this
path does not truncate back to the pre-write offset, recordings exceeding limits such as FAT32's can
end with a partial TS access unit while reporting size_limit.
Agent Prompt
## Issue description
A TS write can partially succeed before returning `EFBIG` at a filesystem-specific size ceiling. The recorder reports `size_limit` without truncating the partial access unit, contradicting the API guarantee that the file ends on a frame boundary.

## Issue Context
`RECORDER_OFF_T_CEILING` only covers the ABI offset limit and is `UINT64_MAX` in the shipped build. The raw recorder already records the frame offset and calls `ftruncate()` after failed writes; the TS recorder needs equivalent behavior.

## Fix Focus Areas
- src/star6e_ts_recorder.c[444-484]
- src/star6e_recorder.c[251-296]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

A 32-bit glibc build leaves off_t 32-bit and open() without O_LARGEFILE, so
the kernel refuses any write crossing 2^31-1 with EFBIG no matter what the
filesystem allows.  A recording on exFAT died at exactly 2147483647 bytes,
twice, and reported "write_error" -- which reads as bad media and sent the
diagnosis to the SD card.  `record.maxMB` above ~2047 was therefore a knob
that validated, applied over MUT_RESTART, and then silently could not work.

Build with -D_FILE_OFFSET_BITS=64.  That is the whole fix: it removes the
ceiling from open(), and equally from the stat()/fstat() calls in
venc_recordings.c and venc_httpd.c that return EOVERFLOW for a >2 GB file
and would otherwise hide such a recording from the listing and the download
path.  The flag has to be on every translation unit or the link silently
mixes two off_t layouts, so it goes in COMMON_CFLAGS and is mirrored into
HOST_CFLAGS to keep the tested ABI equal to the shipped one.  Note that a
CFLAGS change does not invalidate objects: this needs a clean build.

The rest is so the knob cannot lie again if that flag is ever lost:

- check_rotation() rotates on whichever binds first, the operator's limit or
  the one off_t can reach.  A configured max_mb above the ceiling now yields
  more segments instead of a dead recorder.  Inert at 64-bit off_t.
- A segment can only be cut on an IRAP, and the IDR request is bounded, so a
  GDR stream can carry one past any threshold with no cut coming.  Rather
  than walk into an EFBIG that truncates mid-AU, stop on the frame boundary
  with the file intact.
- EFBIG gets its own arm in all three recorders and its own stop reason,
  RECORDER_STOP_SIZE_LIMIT ("size_limit"), because nothing failed -- a
  ceiling is not an I/O error and should not read as one.
- The inactive branch of the record status filled in only stop_reason, so a
  recorder that stopped by itself answered {path:"", frames:0, bytes:0}.  It
  now carries the path, bytes, frames and segments from the snapshot the
  reason came from -- for a non-manual stop that is the entire diagnosis.

contract_version 0.29.0 -> 0.30.0: the stop_reason enum gains a value and the
inactive status payload changes meaning.  Both are additive under the
contract's own governance rules.  VERSION 0.82.0 -> 0.83.0.

Docs: maxSeconds/maxMB of 0 were documented as "no limit" but the runtime
only overrides its compiled-in default when the value is > 0, so 0 means
300 s / 500 MB.  Also state that rotation is TS-only; format "hevc" ignores
both thresholds, which is filed separately.

Device-verified on SSC338Q (.232) against the real recorder core -- three
arms of one harness, same 58 GB FAT32 card, same deterministic input, only
the build differing:

  stock master, no flag   EFBIG "File too large" at 34084 frames
                          file on disk exactly 2147483647 -- the report,
                          reproduced byte for byte
  patched, no flag        segment cut at 2147212672, second segment opened,
                          2306867536 bytes over 2 segments, exit 0
  patched + flag          2306867536 bytes in ONE segment, exit 0

The last two wrote identical totals from identical input, so segmentation is
the only variable between them.  Note the stock arm's counter stopped at
2147464968 while the file is 2147483647: the failing write was partial, which
is the mid-AU truncation the new frame-boundary stop exists to prevent.

Separately verified on .232 that a 32-bit off_t build cannot stat() or fstat()
a 3 GB file at all (EOVERFLOW), so fixing only open() would have produced
recordings the API then hides.

CV610 was checked and is not affected: measured on .181, musl is 64-bit off_t
and forces O_LARGEFILE regardless of the flag.  That negative comes from a
probe whose raw-openat control DID fail with EFBIG on the same kernel, so the
instrument was shown able to detect the positive before its negative was
trusted.

Fixes the root cause reported in OpenIPC#118.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U1w7gRc6ASpa7P7HRCBQEw
@snokvist
snokvist force-pushed the fix/recorder-largefile branch from bbd1c88 to ef7119d Compare September 6, 2026 09:53
@snokvist

snokvist commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Qodo review addressed — all three findings were real, and two were bugs I
introduced. Pushed as an amend to the same branch.

1. Status selects stale recorder — confirmed, fixed. The rule was "take
the TS reason unless it is manual", which guesses from a value that says
nothing about which session ended last. A retained write_error from an
earlier TS run would outrank a later, cleanly stopped hevc recording — and
because this PR made the counters follow the reason, it would then have named
that older file's path and byte count too. My change turned a stale field
into a convincingly stale record.

Fixed by selecting on record.format instead. That field is restart_required,
so within one process only one recorder can ever have run and the other
snapshot is zeroed — the ambiguity is removed rather than arbitrated. The
contract wording now says so explicitly.

2. write_video() exceeds the ~80-line guideline — confirmed, fixed.
AGENTS.md:553. Extracted ts_write_muxed(), which takes the ceiling guard
and the error classification; muxing stays the caller's job. write_video()
is now 77 lines.

3. Filesystem limit truncates the access unit — confirmed, and I had
measured it without noticing.
You are right that the guard derives from
sizeof(off_t) and so cannot anticipate FAT32's 4 GB, and that the TS path
lacked the raw recorder's rollback. The evidence was already in this PR's own
table: the unpatched arm's counter stopped at 2147464968 while the file on
disk was 2147483647 — 18679 bytes of half-written access unit. So
size_limit claiming an intact file was wrong on exactly that path.

Fixed with a rollback to segment_bytes, which is the offset of the last
complete frame (the segment opens at 0 and only completed writes are added),
so it costs no lseek.

New coverage: test_ts_size_limit_rolls_back_partial_write reproduces a
below-off_t ceiling with RLIMIT_FSIZE — which is what makes this
host-testable at all, since the off_t ceiling itself is unreachable where
off_t is 64-bit. It asserts the stop is classified size_limit and that
the file ends on a whole number of TS packets. Mutation-checked both ways:
removing the rollback fails 2 assertions, misclassifying EFBIG fails 1.

make test 3028 passed / 0 failed; all three backends build clean from
make clean.

On the two alternatives in the summary — agreed, and for the reason given:
explicit *64 APIs would have to catch stat/fstat in the listing and
download paths as well, which is the half of this that is easy to miss.

@snokvist
snokvist merged commit 588cdfb into OpenIPC:master Sep 6, 2026
8 checks passed
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