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

Add Valgrind GC integration #15304

Merged
merged 21 commits into from
Jun 15, 2023
Merged

Add Valgrind GC integration #15304

merged 21 commits into from
Jun 15, 2023

Conversation

CyberShadow
Copy link
Member

@CyberShadow CyberShadow commented Jun 10, 2023

Resurrecting some old work from 2015.

Hopefully may help with problems such as https://issues.dlang.org/show_bug.cgi?id=23978.

What this does

This integrates Valgrind (specifically, the memcheck tool) with the D GC. It allows catching bad code such as the following:

import core.memory;

void main()
{
	auto arr = new int[3];
	GC.free(arr.ptr);
	arr[1] = 42;
}

The integration makes it so that harmless memory operations (such as those done by the GC) don't emit spurious warnings, and invalid memory operations (such as in the example above) do emit warnings.

How to use it

WIP (will add docs) See changelog; the goal is to be able to do essentially

dmd -debug=VALGRIND path/to/conservative/gc.d my_program.d

to get the integration. (I.e., add the GC-related parts of Druntime to the compilation of your program so that it is rebuilt with -debug=VALGRIND).

@dlang-bot
Copy link
Contributor

Thanks for your pull request, @CyberShadow!

Bugzilla references

Your PR doesn't reference any Bugzilla issue.

If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog.

Testing this PR locally

If you don't have a local development environment setup, you can use Digger to test this PR:

dub run digger -- build "master + dmd#15304"

@CyberShadow
Copy link
Member Author

CyberShadow commented Jun 10, 2023

This relies on the Valgrind API, which is defined in the valgrind/valgrind.h and valgrind/memcheck.h header files. If Valgrind is installed, the header files can be used in C programs with e.g. #include <valgrind/valgrind.h>.

How we can approach this for the D side:

  1. Make everything Valgrind-related opt-in. Nothing is changed requirements-wise, however, Valgrind integration can't be tested in the regular CI flow, and using the integration will be non-trivial (e.g. require rebuilding Druntime/Phobos or invoking a C compiler).
  2. Add Valgrind as a build dependency for our Linux builds (release and CI), and use /usr/include/valgrind/memcheck.h. Is this OK?
  3. Vendor the header files. They are BSD-licensed, so I think this should be OK too.
  4. Use ImportC to use the C header files at user program build time. Unfortunately ImportC isn't ready for this, e.g. for one thing it would need to understand GNU assembler syntax...
  5. Port the necessary parts of the header files to D. I would not recommend this as they are full of platform-specific details.

I'm going to go with option 3 as it seems the most flexible and least troublesome.

CyberShadow and others added 2 commits June 10, 2023 13:02
This is not portable, and memcheck complains about this.
Add the BSD-licensed C header files which provide the API for
controlling Valgrind from within programs executed under it.

The files are from Valgrind v3.21.0.
We will use these in the GC implementation to tell Valgrind which
memory operations are OK or not.
Allow the conservative GC to scan memory, whether it has been
initialized by the application or not.
Copy link
Contributor

@dkorpel dkorpel left a comment

Choose a reason for hiding this comment

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

Nice work!

void _d_valgrind_make_mem_defined (const(void)* addr, size_t len);
}

void makeMemNoAccess (const(void)[] mem) nothrow { _d_valgrind_make_mem_noaccess (mem.ptr, mem.length); }
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add ddoc comments to public functions

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks! Done.

@ljmf00
Copy link
Member

ljmf00 commented Jun 11, 2023

Awesome! CC @JohanEngelen does this gives some lights about compiler-rt ASAN being aware of the GC? I think valgrind interfaces are very similar

@WalterBright
Copy link
Member

Use ImportC to use the C header files at user program build time. Unfortunately ImportC isn't ready for this, e.g. for one thing it would need to understand GNU assembler syntax...

ImportC currently replaces instances of GNU asm with assert(0). This enables compilation of the functions with inline asm in them, they just will assert if they try to execute the inline asm. The rest of the .h file will work. I haven't looked at the .h files here, but it seems to me that a manual conversion to D will wind up doing the same thing. Create a .c file that instantiates all the asm code, exposing them as functions, and then D can access them.

I'm interested in any and all problems with ImportC.

@CyberShadow
Copy link
Member Author

ImportC currently replaces instances of GNU asm with assert(0). This enables compilation of the functions with inline asm in them, they just will assert if they try to execute the inline asm.

Okay, but that doesn't help us very much because the critical parts are all in inline assembly.

The rest of the .h file will work.

The remaining parts (about 20%) wrap the assembly code to create the public API.

I haven't looked at the .h files here, but it seems to me that a manual conversion to D will wind up doing the same thing.

I don't understand what that would mean in this context.

Create a .c file that instantiates all the asm code, exposing them as functions, and then D can access them.

This is what the implementation in this PR does.

@CyberShadow
Copy link
Member Author

CyberShadow commented Jun 12, 2023

I think it would help if I explained how the Valgrind client API works.

Valgrind is a binary retranslation tool. You give it a compiled program containing x86_64 etc. instructions, and before running it, Valgrind translates it to a different x86_64 program, with some instrumentation added. As such, it is something akin to both a VM and a JIT.

As it is translating the program, it instruments it for additional functionality, depending on the tool used. Among other things, the memcheck tool tracks, on a bit-by-bit level, which variables (including in the stack and registers) hold defined values, and which do not. So, if the program were to dereference a value from a freshly-returned malloc call, or a byte from a struct padding, and then make a decision based on that value, memcheck will flag it as an error.

The client API controls the Valgrind retranslator. When Valgrind encounters these special instructions while translating our program's code, it will understand these as requests to adjust its internal state accordingly (i.e. to pretend that a copy of some value is defined just this once, or mark some memory range as undefined). The header file contains these instructions.

Worth pointing out that the instructions are "junk" no-op instructions that don't do anything if the program isn't running under Valgrind, so one thing we could actually do is enable Valgrind for our debug Phobos builds. At the cost of a very small slowdown, that will allow users to just run debug builds of D programs under Valgrind and get correct diagnostics about invalid memory operations.

@CyberShadow CyberShadow force-pushed the valgrind branch 3 times, most recently from c31b0d2 to ed43197 Compare June 12, 2023 08:29
CyberShadow and others added 6 commits June 12, 2023 09:24
The two share the same goal: mark memory which should not be accessed
any more.
The GC stores free lists in the cells of the objects on the list.

We would like to allow the GC to work with these lists, but still
raise a warning if application code attempts to access them.
Freshly allocated memory should be writable but not readable.

Explicitly deallocated or GC-ed memory should be neither readable or
writable.
Run the Druntime Valgrind integration tests.

libc6-dbg is needed to allow Valgrind to redirect certain functions.
Although the GC is the primary user, this is not a restriction of
these Valgrind API wrappers.
Trick the optimizer to pretend we're doing something with the result
of those invalid memory accesses.
@JohanEngelen
Copy link
Contributor

Awesome! CC @JohanEngelen does this gives some lights about compiler-rt ASAN being aware of the GC? I think valgrind interfaces are very similar

Indeed. I have similar unfinished work.
Adding ASAN support is probably straightforward because the marking/poisoning/invalidation code is separated into special functions (including the "write to this invalid memory area" is very useful! undefinedWrite).

@WalterBright WalterBright merged commit 7cdae6e into dlang:master Jun 15, 2023

```
git clone -b v2.105.0 --depth=1 https://github.com/dlang/dmd
dmd -g -debug=VALGRIND program.d -Idmd/druntime/src dmd/druntime/src/{core/internal/gc/impl/conservative/gc,rt/lifetime,etc/valgrind/valgrind}.d
Copy link
Contributor

@dkorpel dkorpel Jun 16, 2023

Choose a reason for hiding this comment

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

I tried this, and it results in undefined reference to '_d_valgrind_make_mem_noaccess' etc., because it doesn't tell you to cc -c druntime/src/etc/valgrind/valgrind.c -o valgrind.a and include that as a source file. Other than that, works like a charm!

This documentation should not be only in the changelog.

Copy link
Member Author

Choose a reason for hiding this comment

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

The C parts should be compiled into libphobos.a everywhere on Posix. Check that you're using libphobos.a built from master, maybe?

I think valgrind.d could have version (Posix): instead of debug (VALGRIND): at the top to avoid having to recompile it too, but I did not experiment with it too much. There's some issues like the D file pulling in the C declarations even when the D declarations are not used by anything on some platforms...

Copy link
Contributor

Choose a reason for hiding this comment

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

You're right, the dmd command launches v2.104.0 from my package manager, but when I use dmd built from master, it includes the valgrind symbols.

@CyberShadow
Copy link
Member Author

CyberShadow commented Jun 16, 2023

This documentation should not be only in the changelog.

Agreed. I think we also need a centralized place for us to document the --DRT switches. Maybe Druntime should have a documentation folder with just plain DDoc files, or something like that.

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.

6 participants