Skip to content
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

build: ubsan fixes #4254

Open
wants to merge 8 commits into
base: v1.x
Choose a base branch
from
Open

build: ubsan fixes #4254

wants to merge 8 commits into from

Conversation

mizvekov
Copy link
Contributor

MSVC does not actually support ubsan. There is a long-standing ticket requesting this:
https://developercommunity.visualstudio.com/t/add-support-for-ubsan/840750

There are no known compilers that currently accept the /fsanitize=undefined spelling. clang-cl accepts -fsanitize..., same as regular clang.

@mizvekov mizvekov marked this pull request as draft December 17, 2023 01:52
Copy link
Member

@bnoordhuis bnoordhuis left a comment

Choose a reason for hiding this comment

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

I don't have time to look at this in detail today but apropos the linux and macos test failures:

 # exit code 6
# Output from process `ipc_send_zero`:
# /Users/runner/work/libuv/libuv/src/unix/stream.c:701:15: runtime error: applying zero offset to null pointer
# SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /Users/runner/work/libuv/libuv/src/unix/stream.c:701:15 in 
# exit_cb
# Assertion failed in /Users/runner/work/libuv/libuv/test/test-ipc.c on line 95: `term_signal == 0` (6 == 0)

Does that mean ubsan wasn't running before? The failure looks legitimate; uv__write_req_update() should check buf->base != NULL.

CMakeLists.txt Show resolved Hide resolved
@mizvekov
Copy link
Contributor Author

Does that mean ubsan wasn't running before?

It was running, but in recovery mode, which means the default ubsan runtime, with the default settings, was only printing the problems, but not crashing the program.

The test-runner hides output unless the test fails, so that is why I believe you didn't see these failures before.

The failure looks legitimate; uv__write_req_update() should check buf->base != NULL.

Yep, fixed locally, just didn't push the commit yet.

I am investigating one more macOS ubsan failure I see running on my machine, but not on CI:

build/dbg/uv_run_tests_a osx_select_many_fds osx_select_many_fds
got some input
with a couple of lines
feel pretty happy
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.2.sdk/usr/include/sys/_types/_fd_def.h:93:4: runtime error: index 47 out of bounds for type '__int32_t[32]' (aka 'int[32]')
    #0 0x100847a64 in uv__stream_osx_select stream.c:174
    #1 0x1873b2030 in _pthread_start+0x84 (libsystem_pthread.dylib:arm64e+0x7030)
    #2 0x1873ace38 in thread_start+0x4 (libsystem_pthread.dylib:arm64e+0x1e38)

SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.2.sdk/usr/include/sys/_types/_fd_def.h:93:4 in

@mizvekov
Copy link
Contributor Author

mizvekov commented Dec 20, 2023

Is it possible to get macos-arm64 github workflow working?
No runners are picking up the job, so perhaps there is something missing on the project setup side?

@bnoordhuis
Copy link
Member

Is it possible to get macos-arm64 github workflow working?

Are those even available when you're not on a paid plan?

@mizvekov
Copy link
Contributor Author

Nevermind, I see that the closest to my machine (macOS 14 arm64) we have CI available for free is macOS 13 intel.

Adding it does expose a different test failure I had seen locally, but it's not ubsan related, so I will split that off for another PR.

And about the ubsan failure on osx_select_many_fds, I failed to realize before that this test is even skipped on CI due to lack of /dev/tty.

@mizvekov mizvekov marked this pull request as ready for review December 20, 2023 16:36
@mizvekov mizvekov changed the title Draft: build: ubsan fixes build: ubsan fixes Dec 20, 2023
Copy link
Member

@bnoordhuis bnoordhuis left a comment

Choose a reason for hiding this comment

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

I have to admit, while I appreciate the work you've done, a number of these changes don't look Obviously Correct(TM) to me.

As in: they quite possibly are, but not obviously so, and that makes me hesitant to sign off on it. Maybe you can elaborate on why you made specific changes, and why you took specific approaches instead of others?

#define BILLION ((int64_t) 1000 * 1000 * 1000)
#define NSEC_PER_TICK 100
#define TICKS_PER_SEC ((long)1e9 / NSEC_PER_TICK)
static const int64_t WIN_TO_UNIX_TICK_OFFSET = 11644473600 * TICKS_PER_SEC;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
static const int64_t WIN_TO_UNIX_TICK_OFFSET = 11644473600 * TICKS_PER_SEC;
static const int64_t WIN_TO_UNIX_TICK_OFFSET = 11644473600LL * TICKS_PER_SEC;

Otherwise it overflows; the promotion to int64_t is done after multiplication.

You cast to long on line 101 but sizeof(long) < sizeof(int64_t) on Windows, IIRC. If you cast to int64_t instead, multiplication would work okay because then 11644473600 gets implicitly promoted to int64_t too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The literal 11644473600 is already bigger than LONG_MAX (Which on windows is 2,147,483,647).
It needs about 35 bits for a signed representation. This means that on those platforms where long is 32 bit's, the literal should have long long type, as that is the next rank, and the smallest that can represent it.

For reference of how this works, see [6.4.4.1]/5 from the C99 draft standard: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf

The type of an integer constant is the first of the corresponding list in which its value can
be represented

And the corresponding list in the table for an unsiffixed decimal constant:

int
long int
long long int

src/win/getaddrinfo.c Outdated Show resolved Hide resolved
Comment on lines 124 to 137
align_offset(&cur_off, sizeof(void*));
cur_off += sizeof(struct addrinfo);
/* TODO: This alignment could be smaller, if we could
portably get the alignment for sockaddr. */
align_offset(&cur_off, sizeof(void*));
cur_off += addrinfow_ptr->ai_addrlen;
Copy link
Member

Choose a reason for hiding this comment

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

Arguably easier to read if you make align_offset() return the new value instead of mutating it in place:

Suggested change
align_offset(&cur_off, sizeof(void*));
cur_off += sizeof(struct addrinfo);
/* TODO: This alignment could be smaller, if we could
portably get the alignment for sockaddr. */
align_offset(&cur_off, sizeof(void*));
cur_off += addrinfow_ptr->ai_addrlen;
cur_off += align_offset(cur_off, sizeof(void*));
cur_off += sizeof(struct addrinfo);
/* TODO: This alignment could be smaller, if we could
portably get the alignment for sockaddr. */
cur_off += align_offset(cur_off, sizeof(void*));
cur_off += addrinfow_ptr->ai_addrlen;

I don't understand the TODO, btw. I'm also not 100% sure the alignment computations here are correct but I don't know exactly how the objects are laid out in memory.

@libuv/windows you should take a careful look at this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here we are pessimizing by assuming sockaddr needs pointer alignment, but I have seen that on some platforms it needs only uint16_t alignment or so.

The problem is that C89 does not have a ready made facility for obtaining alignment requirement of a given type.
I felt introducing that on this patch would be bigger change with more involved review than needed, considering existing code already suffers from this limitation, and if we introduced such a facility, there likely would be many other unrelated areas of libuv we would want to use it on.

Copy link
Member

Choose a reason for hiding this comment

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

I would guess that C89 can express it as an offsetof computation, roughly as follows: alignof(t) = offsetof(struct { char x; t y }, y) but it does seem pretty annoying

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That does not work when compiling as C++, and as C it does, but GCC gives bogus warning about missing semicolon at the end of the struct definition. But you can't possibly put a semicolon there anyway: https://godbolt.org/z/GYWorbKMY

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A bunch of other C compilers don't accept this as well. MSVC, Compcert, tiny c, sdcc, to name some.
https://godbolt.org/z/GYWorbKMY

src/win/process-stdio.c Outdated Show resolved Hide resolved
src/win/fs.c Outdated Show resolved Hide resolved
ts->tv_sec = (long) (filetime / (10 * MILLION));
ts->tv_nsec = (long) ((filetime - ts->tv_sec * 10 * MILLION) * 100U);
filetime -= WIN_TO_UNIX_TICK_OFFSET;
ts->tv_sec = filetime / TICKS_PER_SEC;
Copy link
Member

Choose a reason for hiding this comment

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

I don't really see why this implicit truncation is better than the previous explicit cast

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well there is an assignment from an expression of long long type into a long variable, for sure, but the important detail here is if there is really truncation.
Truncation of a signed integer with with a simple cast like that is still UB, and UBSAN will catch that.

If we want defined behaviour, with either saturation or bit discarding assuming two's complement for example, we have to do something different.

Copy link
Member

Choose a reason for hiding this comment

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

IIRC, this field uint64_t for clients that already upgraded to libuv v2 (though it is unreleased) to avoid this exact problem -- as indeed it will become a significant problem in about a decade for existing clients of v1

read_flags,
write_flags,
0,
(unsigned long long)&fds[0]);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
(unsigned long long)&fds[0]);
(uintptr_t) &fds[0]);

nit: long long is not the size of a pointer

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But we are not trying to fit a pointer in an integer per se. We want a random number, to be used for generating a unique pipe name, which avoids collisions.

I think unsigned long long has enough bits here, it's unlikely we would have so many open pipes we would see collisions in practice. The (pre-existing) hack though is trying to use a heap pointer as a source of randomness. Granted, if pointer size were larger than long long, we would be discarding some bits, but do you really think this is a problem?

Copy link
Member

Choose a reason for hiding this comment

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

That's right, but the HANDLE pointer needs to be cast first to an uintptr_t-sized object before up-or-downcasting the bytes (to make the compiler happy). We may want to consider using SystemFunction036 or uv_random eventually, since this is indeed a pretty ridiculous hack (which I say as the person who wrote it).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

While the standard does not say anything directly about pointer sizes wrt integer sizes, unsigned long long is the largest integer available in standard C.

So if for a given platform a pointer could round trip through a suitably large integer, this integer would necessarily be at most as large as ULL.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, that is fine, but the compiler won't like it if you cast directly to a different size

src/win/pipe.c Show resolved Hide resolved
src/win/process-stdio.c Outdated Show resolved Hide resolved
src/win/getaddrinfo.c Outdated Show resolved Hide resolved
MSVC does not actually support ubsan. There is a long-standing ticket
requesting this:
https://developercommunity.visualstudio.com/t/add-support-for-ubsan/840750

There are no known compilers that currently accept the
`/fsanitize=undefined` spelling. clang-cl accepts `-fsanitize...`,
same as regular clang.

Also passes no-sanitizer-recover so that tests actually fail.
Don't use CONTAINING_RECORD macro from WinSDK, as it doesn't use the
right trick which avoids member access on null pointer.

Fixes:
```
src/win/req-inl.h:86:10: runtime error: member access within null pointer of type 'uv_req_t' (aka 'struct uv_req_s')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior D:/a/libuv/libuv/src/win/req-inl.h:86:10
```
Refactor / cleanup arithmetic for unix -> win filetime conversion
in order to avoid multiplication overflow.

Fixes:
```
src/win/fs.c:106:48: runtime error: signed integer overflow: 1702781567 * 10 cannot be represented in type 'long'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior src/win/fs.c:106:48 in
```
Don't call functions through different function type.

Fixes:
```
src/win/udp.c:537:5: runtime error: call to function req_cb through pointer to incorrect function type 'void (*)(struct uv_udp_send_s *, int)'
test\test-ref.c:66: note: req_cb defined here
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior src/win/udp.c:537:5 in
```
When accessing HANDLEs within the stdio buffer, use memcpy / memset in order to respect alignment.

Fixes:
```
src/win/process-stdio.c:197:5: runtime error: store to misaligned address 0x0230ee72d107 for type 'HANDLE' (aka 'void *'), which requires 8 byte alignment
0x0230ee72d107: note: pointer points here
  00 00 cd cd cd  cd cd cd cd cd cd cd cd  cd cd cd cd cd cd cd cd  cd cd cd cd cd cd cd fd  fd fd fd
              ^
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior src/win/process-stdio.c:197:5 in
```
Reworks buffer alignment handling to respect requirements.

Fixes:
```
src/win/getaddrinfo.c:157:23: runtime error: member access within misaligned address 0x0290e4c6a17c for type 'struct addrinfo', which requires 8 byte alignment
0x0290e4c6a17c: note: pointer points here
  00 00 00 00 cd cd cd cd  cd cd cd cd cd cd cd cd  cd cd cd cd cd cd cd cd  cd cd cd cd cd cd cd cd
              ^
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior src/win/getaddrinfo.c:157:23 in
```
Changes "random" representation from pointer to number.

Fixes:
```
src/win/pipe.c:234:11: runtime error: applying non-zero offset to non-null pointer 0xffffffffffffffff produced null pointer
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior src/win/pipe.c:234:11 in
```
Avoids performing pointer arithmetic on null pointer.

Fixes:
```
src/unix/stream.c:701:15: runtime error: applying zero offset to null pointer
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /Users/runner/work/libuv/libuv/src/unix/stream.c:701:15 in
```
@mizvekov
Copy link
Contributor Author

I have to admit, while I appreciate the work you've done, a number of these changes don't look Obviously Correct(TM) to me.

As in: they quite possibly are, but not obviously so, and that makes me hesitant to sign off on it. Maybe you can elaborate on why you made specific changes, and why you took specific approaches instead of others?

Thanks for the input. I gave it another round of edits, but otherwise please point to specific commits if you want more explanation on them.

@mizvekov
Copy link
Contributor Author

Hello, just a friendly ping on this MR.

@mizvekov
Copy link
Contributor Author

Hello, another ping.

Would you prefer we expedite this by splitting off less controversial commits to another MR? If you think that's possible, please let me know which commits.

@vtjnash
Copy link
Member

vtjnash commented Feb 12, 2024

I have honestly completely forgotten where we left off on this, after a very busy month. If there are small chunks you think would be easier to merge separately, by all means we can do those first.

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.

None yet

3 participants