-
Notifications
You must be signed in to change notification settings - Fork 213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replace some assertions by AVIF_ASSERT() #1958
Conversation
include/avif/internal.h
Outdated
@@ -59,6 +59,17 @@ static inline void avifBreakOnError() | |||
} \ | |||
} while (0) | |||
|
|||
// Same as AVIF_CHECKERR() but also assert() in debug build configuration. | |||
// Can be used instead of assert() for extra security. | |||
#define AVIF_ASSERT(A, ERR) \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a big fan of the name. How about AVIF_ASSERT_AND_RETURN ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not AVIF_ASSERT_OR_RETURN
indeed
src/read.c
Outdated
@@ -566,7 +566,7 @@ static avifResult avifCodecDecodeInputFillFromDecoderItem(avifCodecDecodeInput * | |||
} | |||
} | |||
if (remainingSize > 0) { | |||
assert(layerCount == 3); | |||
AVIF_ASSERT(layerCount == 3, AVIF_RESULT_UNKNOWN_ERROR); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should only use assert()
for conditions that must be true by logical reasoning. We should not use assert()
to detect bad inputs or environment errors (e.g., filesystem errors). Under this policy, an assertion failure indicates a bug in our code. If we want to return an error in that case, let's add a new error code AVIF_RESULT_INTERNAL_ERROR
.
Another issue is that some assertion failures imply that memory may have been corrupted or our data structures may be in an inconsistent state, so it may be unsafe to return an error and continue execution. This is why in Google's internal repository and in Chrome, there is a CHECK
that is always enabled. We may want to consider adding our equivalent of CHECK
. It would be simpler than analyzing whether it is safe to return an error and continue execution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More on my suggestion of adding AVIF_RESULT_INTERNAL_ERROR
:
AVIF_RESULT_UNKNOWN_ERROR
is equivalent to absl::StatusCode::kUnknown
, defined as follows:
// kUnknown [...] indicates an unknown error occurred. In
// general, more specific errors should be raised, if possible. Errors raised
// by APIs that do not return enough error information may be converted to
// this error.
AVIF_RESULT_INTERNAL_ERROR
is equivalent to absl::StatusCode::kInternal
, defined as follows:
// kInternal [...] indicates an internal error has occurred
// and some invariants expected by the underlying system have not been
// satisfied. This error code is reserved for serious errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should only use
assert()
for conditions that must be true by logical reasoning. We should not useassert()
to detect bad inputs or environment errors (e.g., filesystem errors). Under this policy, an assertion failure indicates a bug in our code. If we want to return an error in that case, let's add a new error codeAVIF_RESULT_INTERNAL_ERROR
.
Done with AVIF_CHECKERR(..., AVIF_RESULT_INTERNAL_ERROR)
.
Thank you for checking abseil status code meanings.
Another issue is that some assertion failures imply that memory may have been corrupted or our data structures may be in an inconsistent state, so it may be unsafe to return an error and continue execution. This is why in Google's internal repository and in Chrome, there is a
CHECK
that is always enabled. We may want to consider adding our equivalent ofCHECK
. It would be simpler than analyzing whether it is safe to return an error and continue execution.
Terminating execution may be safer but crashes are inconvenient for users.
In the example mentioned in the PR description, I believe libavif refusing to decode a few rare images (color grid but no alpha grid) is better than crashing the AVIF decoding, potentially also the whole service using libavif as a dependency. Most of the checks enforced in this PR are preventive ("alpha should be defined before being used" etc). Returning an error there seems fine.
I suggest replacing most assertions in libavif by AVIF_CHECKERR(..., AVIF_RESULT_INTERNAL_ERROR)
and having some kind of CHECK
in critical places, if necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: I just read the message as the beginning of this pull request. Your AVIF_RESULT_ASSERTION_FAILED
is essentially the AVIF_RESULT_INTERNAL_ERROR
I suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. I suggest some tweaks of the comment and error message for AVIF_RESULT_INTERNAL_ERROR
.
I also think that the conditions that can be easily shown to be true can be safely checked by assert()
, but I wasn't able to inspect all the changes in this pull request. Note: Using assert()
to check such conditions is just a code size optimization, and the effort to prove a condition is true may depend on familiarity with the code or knowledge of the underlying specs, so "easily shown to be true" may not be absolute.
src/avif.c
Outdated
@@ -103,6 +103,7 @@ const char * avifResultToString(avifResult result) | |||
case AVIF_RESULT_OUT_OF_MEMORY: return "Out of memory"; | |||
case AVIF_RESULT_CANNOT_CHANGE_SETTING: return "Cannot change some setting during encoding"; | |||
case AVIF_RESULT_INCOMPATIBLE_IMAGE: return "The image is incompatible with already encoded images"; | |||
case AVIF_RESULT_INTERNAL_ERROR: return "Some invariants have not been satisfied"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message can just say "Internal error".
include/avif/avif.h
Outdated
@@ -186,10 +186,11 @@ typedef enum AVIF_NODISCARD avifResult | |||
AVIF_RESULT_OUT_OF_MEMORY = 26, | |||
AVIF_RESULT_CANNOT_CHANGE_SETTING = 27, // a setting that can't change is changed during encoding | |||
AVIF_RESULT_INCOMPATIBLE_IMAGE = 28, // the image is incompatible with already encoded images | |||
AVIF_RESULT_INTERNAL_ERROR = 29, // some invariants have not been satisfied |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I suggest we have a short primary definition "internal error", followed by an explanation that say "some invariants have been satisfied". Ideally the definition should make it clear that this error indicates a bug in libavif.
src/read.c
Outdated
@@ -566,7 +566,7 @@ static avifResult avifCodecDecodeInputFillFromDecoderItem(avifCodecDecodeInput * | |||
} | |||
} | |||
if (remainingSize > 0) { | |||
assert(layerCount == 3); | |||
AVIF_CHECKERR(layerCount == 3, AVIF_RESULT_INTERNAL_ERROR); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only inspected this change, and this is an example of condition that can be safely checked with assert()
. The reason is that one can prove this condition is true by analyzing only a few lines of code (specifically, line 544 and lines 551-568).
It seems that AVIF_CHECKERR(..., AVIF_RESULT_INTERNAL_ERROR)
is more appropriate for conditions that can only be proved true by analyzing multiple functions or complicated code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yannis: I just realized that I made a mistake in the decision to approve this pull request.
Influenced by the AVIF_ASSERT()
in the title of this pull request, I somehow thought the AVIF_CHECKERR(condition, AVIF_RESULT_INTERNAL_ERROR)
in this pull request would be backed by an assert()
in Debug build, so that this pull request would be essentially a no-op in Debug build and a win (because of the error handling) in Release build.
But this pull request replaces assert()
with error handling in both Debug and Release builds. As a debugging aid, this is a downgrade in Debug build because we won't get an error message (emitted by assert()
) right when a bug is detected. If we can reproduce the bug, this is just an inconvenience. But if we can't reproduce the bug, this is a loss of useful debugging information.
So it may be better to convert fewer assertions so that we can inspect them. For example, the assert(itemID != 0)
in avifMetaFindOrCreateItem()
can be converted.
In my opinion this is easily caught by using |
What I meant is that if we can't reproduce the bug, printing an error message and aborting the process immediately when a bug is detected is more helpful to debugging than returning In any case, all options for this issue have pros and cons, so it is fine to try this approach. In my experience, people have a hard time agreeing on how assertions should be used. So I think it is good to adopt a policy or guidelines for assertions from a reputable software organization (similar to style guides for programming languages). I suggest we adopt Chrome's policy for assertions ( |
A complication is that an |
Ah, got it. Good point. This is also true for any returned error, not only the current assertions. So overall logging could be improved in libavif.
I am still uneasy with crashing as a default behavior. It may be fine in a Chromium environment but annoying for other users of libavif.
Indeed. I suggest this PR to introduce |
Yannis: Yes, your suggestion of adding |
include/avif/internal.h
Outdated
@@ -59,6 +59,17 @@ static inline void avifBreakOnError() | |||
} \ | |||
} while (0) | |||
|
|||
// Same as AVIF_CHECKERR() but also assert() in debug build configuration. | |||
// Can be used instead of assert() for extra security in release builds. | |||
#define AVIF_ASSERT_OR_RETURN(A, ERR) \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Delete the ERR
parameter. This macro should always use AVIF_RESULT_INTERNAL_ERROR
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This macro should always use
AVIF_RESULT_INTERNAL_ERROR
.
I agree but at calling site it is not as readable in my opinion. What about a static_assert()
to make sure ERR
is always AVIF_RESULT_INTERNAL_ERROR
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you mean. Here is an example:
AVIF_ASSERT_OR_RETURN(layerCount == 3)
I think this is caused by the macro name. Perhaps we can rename the macro AVIF_ASSERT_OR_RETURN_ERROR
or even AVIF_ASSERT_OR_RETURN_INTERNAL_ERROR
.
Adding the ERR
parameter back is also fine by me.
static_assert
is only available in C11 or later, so we will need to add an AVIF_STATIC_ASSERT
macro if we want to use static_assert
in our C code.
#define AVIF_ASSERT_OR_RETURN(A) AVIF_CHECKERR((A), AVIF_RESULT_INTERNAL_ERROR) | ||
#else | ||
#define AVIF_ASSERT_OR_RETURN(A) assert(A) | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Informational, not a request for change.] Please see ABSL_HARDENING_ASSERT
. It seems to be Abseil's equivalent of CHECK
.
[skip ci]
[skip ci]
[skip ci]
[skip ci]
This is a general proposal, there are variants:
AVIF_CHECKERR()
directly instead of introducingAVIF_ASSERT()
AVIF_RESULT_ASSERTION_FAILED
instead of usingAVIF_RESULT_UNKNOWN_ERROR
What do you think? If you agree with this initiative, it can be applied to more files in
src/
.Example of why this PR is useful:
AVIF_ASSERT(itemID != 0);
inavifMetaFindOrCreateItem()
could have sped the security investigation around #1922 up.