-
Notifications
You must be signed in to change notification settings - Fork 186
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
snprintf
bounds-safe interface is unhelpful for variable-length null-terminated buffers
#450
Comments
Thank you for the report.
|
Thanks Sulekha for the update (on this and my other recent issues)!
I was talking about the void test() {
char ip4_bin[4];
char buf _Nt_checked[50];
my_inet_ntop(AF_INET, ip4_bin, buf, sizeof buf);
} fails to compile:
To work around this, the caller would have to subtract 1 from I appreciate the work you're doing on better solutions. But what would you recommend that we do right now, e.g., for functions we're adding in #448? Copy and paste the complicated bound expression from |
Here's one proposal for something that we could do right now in order to proceed with porting. I will illustrate this approach for the
NOTE: The macros Here is a test case that shows the two common ways in which the function
The macros serve two purposes:
Comments / Improvements / Alternatives are welcome. |
Hi Sulekha, I really appreciate your help in finding an interim solution so we can proceed with porting right away. I propose we start with a simpler example function with no variable arguments, since variable arguments both cause issues for the macros and require all calls to be unchecked; we'll see that my final proposal below works for variable-argument functions. Here's my example code analogous to yours. I've provided an implementation for the function so we can do simple runtime tests. #pragma CHECKED_SCOPE on
#include <stddef.h>
void sncopy(char * restrict s : itype(restrict _Nt_array_ptr<char>) count(n == 0 ? 0 : n-1),
size_t n,
const char * restrict src : itype(restrict _Nt_array_ptr<const char>)) {
if (n == 0)
return;
size_t i = 0;
_Nt_array_ptr<const char> src2 : count(i) =
_Dynamic_bounds_cast<_Nt_array_ptr<const char>>(src, count(i));
while (i < n - 1 && src2[i] != '\0') {
s[i] = src2[i];
i++;
}
while (i < n - 1) {
s[i] = '\0';
i++;
}
s[n - 1] = '\0';
}
#define chkc1_sncopy(s, n, src) sncopy(s, n, src)
#define chkc2_sncopy(s, n, src) _Unchecked { sncopy((char *)s, n+1, src); }
void sncopy_test1(void) {
char buf _Nt_checked[50];
chkc1_sncopy(buf, 50, "Hello world - 1");
}
void sncopy_test2(_Nt_array_ptr<char> buf : count(len), size_t len) {
chkc2_sncopy(buf, len, "Hello world - 2");
} This works, although I'd like However, I have two more fundamental concerns about the approach: (1) The void sncopy_test2_bad1(_Nt_array_ptr<char> buf : count(len), size_t len) {
chkc2_sncopy(buf, len, (char*)1);
}
void sncopy_test2_bad2(void) {
char bad_buf _Checked[20];
chkc2_sncopy(bad_buf, 100000000, "Hello world - 3");
} Fortunately, this is easy to fix: instead of a macro, we can use a wrapper function that performs only the single needed void sbcopy(restrict _Nt_array_ptr<char> s : count(len),
size_t len,
restrict _Nt_array_ptr<const char> src) {
size_t len2 = len + 1 == 0 ? 0 : len + 1 - 1;
_Nt_array_ptr<char> s2 : count(len2) = 0;
_Unchecked {
s2 = _Assume_bounds_cast<_Nt_array_ptr<char>>(s, count(len2));
}
// The compiler issues a warning on the third argument that I think is wrong
// and irrelevant to the present discussion.
sncopy(s2, len + 1, src);
}
void sbcopy_test2(_Nt_array_ptr<char> buf : count(len), size_t len) {
sbcopy(buf, len, "Hello world - 2");
} (2) The approach still uses a separate wrapper macro or function for every system function that writes a null-terminated string to a buffer. I still think maintaining all of these wrappers is going to be an unreasonable amount of work. This can be solved by defining a helper function and macro that do just the bounds cast and can be used in combination with any system function: _Nt_array_ptr<char> buf_plus_nt_bounds_cast(_Nt_array_ptr<char> buf : count(len), size_t len)
: count(len + 1 == 0 ? 0 : len + 1 - 1) _Unchecked {
return _Assume_bounds_cast<_Nt_array_ptr<char>>(buf, count(len + 1 == 0 ? 0 : len + 1 - 1));
}
#define BUF_PLUS_NT_ARGS(_buf, _len) buf_plus_nt_bounds_cast(_buf, _len), (_len) + 1
void sncopy_test2_better(_Nt_array_ptr<char> buf : count(len), size_t len) {
sncopy(BUF_PLUS_NT_ARGS(buf, len), "Hello world - 2");
} ( Does the As I previously mentioned at the end of #450 (comment), this would also be a good opportunity to factor out the #define nt_count_for_size(_size) count(_size == 0 ? 0 : _size - 1)
void sncopy(char * restrict s : itype(restrict _Nt_array_ptr<char>) nt_count_for_size(n),
size_t n,
const char * restrict src : itype(restrict _Nt_array_ptr<const char>)); |
We discussed this issue internally. We think that there is something deeper going on here that we need to sort out. We will need some time to think about this. |
to address the issues raised in #499 and #450
|
Thanks Sulekha. I'm studying your changes now, and I'd appreciate if you'd give me a few hours to raise any concerns before you merge the PR.
My proposal would still be
Right, I see you have microsoft/checkedc-clang#218 open for this. But if we use where clauses in the checked declarations for existing C functions, what is your plan to avoid breaking compatibility with existing C code with implicit checked header inclusion? It would be silly for C code to pass a literal 0 as the size, but it might well pass a variable that isn't statically known to be positive (because of course, plain C code won't have a where clause), something like this: int foo(char *buf, int size) {
return snprintf(buf, size, /* some arguments */);
} Is your plan to issue a warning (or no diagnostic at all) instead of an error if this occurs in an unchecked scope? |
We plan to take up this fix in a day or two and we should be done with the fix a week after we start. I really do not prefer to have the
With the fix in place and the macro defined as above, we will also no longer have to define different macros if
Yes, the plan is to issue a warning instead of an error if this occurs in an unchecked scope. |
Sounds like a good plan. I have no further concerns about #456. In #448, I'll plan to copy the new code pattern from Is there an existing issue that covers the |
Ok, thanks! We will file an issue and start work on the simplification of |
… declared in checkedc_extensions.h, and removal of the conditional in the snprintf declaration in stdio_checked.h (#456) * Modified some of the Checked-C-specific declarations of libc functions to address the issues raised in #499 and #450 * Missed adding the modified header files. * Added the _Where clause to more closely resemble the snprintf declaration. * Improvements to the test case. * Incorporated review comments.
I see that microsoft/checkedc-clang#1088 has been filed for |
The bounds-safe interface of
snprintf
(from #309) has a conditional:checkedc/include/stdio_checked.h
Lines 112 to 119 in 4e6e0e4
The compiler seems to evaluate the
count
expression just fine whenn
is a constant, which perhaps is the most common use scenario:But if the count expression is given by a variable (which did occur in one of our ports), the compiler isn't smart enough to verify the bounds and raises a warning:
Since the diagnostic is only a warning, the user could choose to manually review the safety of the call and then ignore the warning. But I assume we're trying to work toward a future in which false positive diagnostics are rare enough that they can be made errors.
One workaround is to use
snprintf_array_ptr
and sacrifice one element of the buffer:Currently
snprintf_array_ptr
is undefined (#449), but that could be fixed. Losing one element probably isn't a big deal in practice, but it still feels a bit ugly.I wondered if removing the conditional might solve the problem:
Then if
n = 0
, we getcount(-1)
, which is a little weird but might not cause any problems. Unfortunately, the compiler doesn't even seem to be able to reason thatlen + 1 - 1 = len
:I thought the compiler was supposed to be able to handle addition and subtraction of constants, but I don't know the precise limitations. In any case, I imagine it would be easier to get the compiler to handle the constants than to get it to handle the conditional. One potential workaround is for the user to change their function interface to take the size including the null terminator:
though this feels a little awkward in Checked C, and it will generate an analogous warning as before if
snprintf_test2
is ever called from another function that takes a bound that doesn't include the null terminator and adds 1 when callingsnprintf_test2
.There are plenty of other standard C functions that write a null-terminated string to a user-supplied buffer with a specified size that includes the null terminator (
inet_ntop
is one example I noticed while reviewing #448), so it would be valuable to have a general solution for them. For the most important functions (such assnprintf
), it might be reasonable to define custom wrapper functions that take the length not including the null terminator. I tentatively proposed the namesbprintf
for that analogue ofsnprintf
; theb
stands for "bound", since we judgedscprintf
(c
for "count") to be too hard to read. (Of course, this would require having a place to put the function definitions: see the end of #449.) However, it probably won't be feasible to provide such wrappers for all standard C functions that take size parameters that include the null terminator.The text was updated successfully, but these errors were encountered: