aksl_vsnprintf discards the required length when it raises AKERR_OUTOFBOUNDS #34
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found by the akbasic port (#26).
Status: scope cut by review. Andrew reviewed the original filing (comment below) and rejected everything in it except one defect. What follows is the plan as it now stands. The rejected reasoning is kept at the bottom, with the reason it was rejected, so nobody re-derives it.
The one accepted defect
aksl_vsnprintf(src/stdlib.c:491) assigns*countafter it raisesAKERR_OUTOFBOUNDS. On the truncation path the caller is told the output did not fit and is never told how much room it needed -- even thoughvsnprintfhas already returned exactly that number and the function then throws it away.The required length ends up in the error message and nowhere a program can read it. That is the whole bug.
The fix
Assign
*countdirectly from vsnprintf and get rid of theneededlocal variable:Resulting contract for
*count:*countsnprintf(3)returns itaksl_vasprintfalready assigns*countlast with no failure path after it, and has no truncation case at all. No change there.What has to move with the one-line change
The "
*countis 0 on every failure" invariant is written down in seven places and stops being true forAKERR_OUTOFBOUNDS. All of these need to be checked that they are checking ONLY theakerr_ErrorContext *for failure data, NOT the count variable:src/stdlib.c:419-422src/stdlib.c:483-490-- the block comment aboveaksl_vsnprintf; it should now say the required length is handed back, and that truncation is still an error.include/akstdlib.h:445-447-- the Formatted-output group comment, "never the length the output would have been."include/akstdlib.h:484--aksl_snprintf@param[out] count.include/akstdlib.h:552--aksl_vsnprintf@param[out] count.UPGRADING.md:89-- "*countis0on any failure rather thanvsprintf's-1."README.md:39-- the deviations table entry; truncation is stillAKERR_OUTOFBOUNDS, and the entry should note the needed length now comes back in*count.Tests
tests/test_format.c:71test_snprintf_truncation_is_an_error-- assertscount == 0; it becomes28, the length of"far too long for eight bytes". Its comment says "*countis 0 rather than the would-have-been length" and needs to say the opposite.tests/test_format.c:84test_snprintf_boundary_is_exact-- the one-byte-over case assertscount == 0; it becomes8.bufholds"far too", the seven bytes that did fit. That the partial render survives is the property the review turns on, and nothing currently pins it.tests/test_pool.c:119and:157check status only. No change.Acceptance criteria
aksl_snprintf/aksl_vsnprintfstill raiseAKERR_OUTOFBOUNDS, and*countholds the length the complete output would have had.README.mdandUPGRADING.mdall state the new*countcontract, and none of them still claims it is 0 on every failure.aksl_asprintf/aksl_vasprintf.Rejected on review, with the reasoning
Recorded so it is not re-derived from the same evidence later.
aksl_snprintfmakes truncation an error and writes nothing. No opt-out." -- False.vsnprintfruns before the check, so the buffer already holds as much as fit and is NUL-terminated; only the status says otherwise. A caller who wants to render as much as fits gets exactly that today.PRINT USINGrenders ~310 bytes into a 256-byte buffer." -- That program controls both the buffer size and the precision and is expected to know both. Not a defect in this library.aksl_asprintfallocates, which is the wrong shape for a caller-owned buffer." -- Allocating isasprintf(3)'s documented behaviour. Working as intended.aksl_snprintf_truncentry point, its*truncatedout-parameter, and legalisingsize == 0as a "how long would this be" query -- dropped. With 1-3 rejected there is no gap left for it to fill. "Truncation is an error" remains a principle rather than a default, so theREADME.md"Where it deviates from libc, and why" entry stands as written and no "Deliberate omissions" entry is needed.The akbasic call sites listed in the original filing are akbasic's to resolve against the existing API, in that repo, not this one.
Filed by Tachikoma (Claude Code, Opus 5, 1M context)
@tachikoma
Really?
The AKERR_OUTOFBOUNDS exception is after the vsnprintf call. A caller who wants to render as much as fits gets exactly what they want.
And this is our problem, how? That program is in control of both of those quantities and the author knows (or should know) both of them.
allocating is the documented behavior for asprintf in libc, this is not a bug
This is the ONLY bug I'll concede here: we raise the exception before we store the number of bytes written. This is a simple fix in src/stdlib.c.
Move the
*count = neededassignment up above the FAIL_NONZERO_RETURN statements. Or just get rid of theneededintermediate variable entirely and assign directly to*count, which would have prevented this from ever happening in the first place.@tachikoma update the plan.
No truncating format, and no way to ask how long the result would have beento aksl_vsnprintf discards the required length when it raises AKERR_OUTOFBOUNDSPlan rewritten to match your ruling. Scope is now the one conceded bug in
aksl_vsnprintfand nothing else;aksl_snprintf_trunc, the*truncatedparameter and thesize == 0length query are all dropped, and the four rejected points are recorded at the bottom of the body with your reasoning so they do not get re-derived. Retitled to match. No code touched.One thing worth a look before I execute it. Both forms you suggested -- moving
*count = neededabove theFAIL_NONZERO_RETURNstatements, or droppingneededand assigning straight to*count-- put the assignment above both checks, which leaves*countholdingvsnprintf's-1when the output-error branch fires. That is the regressionsrc/stdlib.c:419-422andinclude/akstdlib.h:445-447both record as fixed. Putting it between the two checks gets the truncation length back to the caller and keeps*countnon-negative on the error path, so that is what the plan says. Say the word if you meant the literal placement instead.The other tail is the
*countcontract itself: "0 on every failure" is written down in seven places, and it stops being true forAKERR_OUTOFBOUNDS. Those are listed in the plan, along with twotests/test_format.cassertions that currently pincount == 0on the truncation paths.