phobos
phobos copied to clipboard
Safe ref counted
This is off Atila's #8101 . I wanted to create the PR against his fork but GitHub didn't allow that.
I tried to reproduce the safety bug @Geod24 found, but could not. It appears that language updates have automatically corrected that. In fact I had to add an additional @trusted wrapper to the constructor in question so that the language would not prevent it's use with scope data totally. I added a test to verify that the safety bug stays gone.
I also had the destructor to check that dip1000 is enabled, otherwise it now stays @system.
Thanks for your pull request and interest in making D better, @dukc! We are looking forward to reviewing it, and you should be hearing from a maintainer soon. Please verify that your PR follows this checklist:
- My PR is fully covered with tests (you can see the coverage diff by visiting the details link of the codecov check)
- My PR is as minimal as possible (smaller, focused PRs are easier to review than big ones)
- I have provided a detailed rationale explaining my changes
- New or modified functions have Ddoc comments (with
Params:andReturns:)
Please see CONTRIBUTING.md for more information.
If you have addressed all reviews or aren't sure how to proceed, don't hesitate to ping us with a simple comment.
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 + phobos#8368"

import std.stdio;
import std.typecons;
struct Container
{
ubyte[] data;
}
struct Blob
{
ubyte[64] data;
}
void main ()
{
auto ptr = getPtr();
writeln(ptr);
const save = ptr.dup;
auto other = getPtr();
assert(save == ptr);
}
ubyte[] getPtr ()
{
Blob blob;
RefCounted!Container rc;
escape(rc.refCountedPayload(), blob);
assert(rc.refCountedPayload().data !is null);
return rc.refCountedPayload().data;
}
void escape (ref Container rc, ref Blob local)
{
rc.data = local.data;
}
Compiles, runs, crashes.
That might be more of a compiler bug than a RefCounted bug though.
Command used:
$ ./dmd/generated/osx/release/64/dmd -preview=dip1000 -run breakdip1000.d
Commits used:
$ git -C dmd show HEAD
commit 71ba3fa46221daa0539b03ed7e41f454bd6ce326
Author: Iain Buclaw <[email protected]>
Date: Wed Jan 26 15:15:44 2022 +0100
posix.mak: Invoke bash to run the bootstrap script (#13549)
$ git -C druntime show HEAD
commit 33511e263134530a5994a775b03a061ea3f1aa34
Author: Sönke Ludwig <[email protected]>
Date: Mon Jan 24 17:30:16 2022 +0100
Fix GetICMProfile signature
See https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-geticmprofilew
$ git -C phobos show HEAD
commit 90d4336ccd96a096c26c393fe1c081c8fd09433e (HEAD, dukc/safe-ref-counted)
Author: Ate Eskola <[email protected]>
Date: Wed Jan 26 21:43:02 2022 +0200
Addressed the concerns in reviews
Thanks! Yeah, as you suspected it's a compiler bug that can be abused without Phobos at all:
@safe unittest //this compiles
{
void escape (ref ubyte[] arr, ref ubyte[64] local)
{
arr = local;
}
@safe ubyte[] getPtr ()
{
ubyte[64] blob;
ubyte[] arr;
escape(arr, blob);
return arr; // Ahh, just like bad old C days!
}
}
Gotta report, if I don't find it already reported.
Didn't find an existing issue, so submitted: https://issues.dlang.org/show_bug.cgi?id=22709
I have another possible vulnerability in mind, but It's late around here so I'll test it another day. I hope I won't forget what I'm having in mind.
I also looked a bit deeper into the internals of RefCounted, and there are some issues that prevented me from exploiting some other features. For example, opAssign takes its argument by value, not ref. Given the use cases of RefCounted, I think that could lead to some very undesirable behavior, but I'll explore this once the low-hanging compiler bugs are fixed :)
Issue 22709 has been fixed: https://github.com/dlang/dmd/pull/13577
Tried to implement my plan to hack this implementation, along the lines of this:
@safe unittest
{
@safe void assignCrap(RefCounted!(int*) countedRef)
{
int local;
auto anotherRef = countedRef; // I was thinking that anotherRef has shorter lifetime than local...
anotherRef.refCountedPayload() = &local; // ...so that this would compile
}
}
The compiler does not fall for this, nor for some similar variations I tried. I THINK it is because the return attribute in refCountedPayload makes the reference to the payload to be of shorter lifetime than anotherRef, not the payload itself.
That does not mean that this is safe, just that my poor head is running out of cunning with this. Perhaps by using a struct payload and using some member function to play with the lifetimes of the variables it is still hackable.
This is the problematic case:
@safe unittest
{
auto rc = RefCounted!int(123);
(ref int n) {
destroy(rc);
int oops = n; // use-after-free if allowed
}(rc.refCountedPayload);
}
As far as I know, in order to ensure this can't happen in @safe code, either refCountedPayload or the destructor must be @system. Currently, it looks like they are both @safe (though I haven't tested it myself). If so, that means this PR is unsound.
Good catch! That one compiles, and also a simpler variant of it (in my opinion) compiles:
@safe unittest
{
auto rc = RefCounted!int(123);
int* ptr = &rc.refCountedPayload();
destroy(rc);
int oops = *ptr;
}
I think the problem is allowing destruction of rc before end of the scope. The destructor is (as far as we have discovered) @safe if called at the end of the lifetime, but not otherwise. Meaning, this PR might be safe in @live but not otherwise :(.
Worse, I think this applies to about any pattern -dip1000 would enable:
@safe void abuse(Container)()
{ auto cont = Container([1,2,3]);
scope ptr = &cont.front;
destroy(cont);
int oops = *ptr;
}
Worse, I think this applies to about any pattern -dip1000 would enable:
There is one pattern that can work here, which is a callback-based API:
auto ref apply(alias callback, T)(auto ref RefCounted!T this_)
{
// Ensures a reference is held until after `callback` returns
auto hold = this_;
// Explicit `scope` prevents `callback` from escaping a reference
scope ptr = () @trusted { return &this_.refCountedPayload(); }();
return callback(*ptr);
}
// Usage
@safe unittest
{
auto rc = RefCounted!int(123);
rc.apply!((ref n) {
destroy(rc);
int ok = n; // ok - kept alive by `hold`
});
}
The downsides of this approach are that (a) the syntax isn't as nice, and (b) it requires an extra ref count increment/decrement at runtime.
I think the best we can do, without a real ownership/borrowing system in the language (@live doesn't count), is to provide both a @safe callback-based API for users willing to accept the runtime overhead, and a @system API that gives direct access, and puts the responsibility on users not to create dangling references. In other words: make refCountedPayload @system and add a @safe apply.
@pbackus : What if someone calls destroy twice ?
@Geod24 The destructor should reset _refCounted to RefCountedStore.init to ensure that it is idempotent even if destroy!false is used.
There is one pattern that can work here, which is a callback-based API:
Going to do it like that, when I find time for it.
Perhaps there is a way to get the refCountedPayload working in @safe. I'm thinking about making the destructor private, and defining an UDA at DRuntime, let's say noEarlyDestroy. destroy would be @system for types annotated with that attribute. What do you think, would that work? In any case, I'm not going to implement that in this PR.
I'm thinking about making the destructor
private, and defining an UDA at DRuntime, let's saynoEarlyDestroy.destroywould be@systemfor types annotated with that attribute. What do you think, would that work?
A similar idea has been suggested before: https://issues.dlang.org/show_bug.cgi?id=21981
Currently, it would not work, because @safe can bypass private using __traits(getMember). Fixing that issue requires breaking existing code, so it will require a deprecation cycle at the very least.
That feature of __traits(getMember) breaks @safety of pretty much everything anyway, so I think we're best of pretending it does not exist.
Well, it's entirely possible at this point that the issue with __traits(getMember) will never be fixed. That's one of the motivations behind @system variables (DIP 1035): they're a way to block access from @safe code that doesn't rely on private.
Until and unless we have 100% confirmation that private will be made inviolable in @safe code, we should not commit to adding any new language features that rely on private for memory safety.
Until and unless we have 100% confirmation that private will be made inviolable in @safe code, we should not commit to adding any new language features that rely on private for memory safety.
Yeah, since DIP1035 can fix most but not all problems of __traits(getmember), it is within possibility that W&A declare violating privacy via it is an official feature even in @safe, rather than a bug. I agree we need an official decision on that before trying that @noEarlyDestroy thing.
Perhaps a forum topic is in order. On the other hand if DIP1035 gets rejected (I would be disappointed) no discussion is needed IMO: that behaviour is unambiguously a bug.
Ahh, finally found the "convert to draft" button
Mostly done in my local tree. Still trying to get automatic inference of @safe working, it will compile if apply is used in explicit @safe but for some reason if inference is on it stays @system.
I encountered one problem though: consistency. If apply is used on a null Nullable, it will simply do nothing. However if it's a null RefCounted, it will either auto-initialize it or trigger an assertion. Not very consistent. Should I think another name for apply on RefCounted, or alternatively make it no-op on null RefCounted?
Should I think another name for
applyonRefCounted, or alternatively make it no-op on nullRefCounted?
I think renaming it is better in this case, given the difference in how the two versions handle the return value of the callback (wrap it in a Nullable vs return it directly).
A couple ideas:
rc.borrow!((scope ref payload) { /* ... */ });
rc.withPayload!((scope ref payload) { /* ... */ });
borrow sounds good.
Obviously I need to fix the autotest suite at some point, but otherwise ready for more comments now.
Oh no. A dmd test that uses dirEntries is trying to link to unsafe destructor of DirIterator, which it can't find because it just became safe. I'm guessing it happens because the DMD test is compiled without DIP 1000, but the phobos binary is compiled with.
If I'm right, making DirIterator a template should solve the problem. Here goes...
It looks like it's the test runner itself (run.d) that fails to compile. Maybe an easier fix would be to have the DMD build system compile it with -preivew=dip1000 enabled?
It looks like it's the test runner itself (run.d) that fails to compile. Maybe an easier fix would be to have the DMD build system compile it with -preivew=dip1000 enabled?
I think I must avoid that separate compilation issue in any case. A Phobos user that happens to not use -dip1000 would face the same issue.
Ping @dukc , what is the status of this?
Ping @dukc , what is the status of this?
Waiting for me to get the test suite green. I'm going to but it may take long because I don't currently have much extra time.
Progress update: the thing I'm struggling with is that while I can indeed solve the linking issue with dirEntries itself by making it a template, DirIterator has some generated destructors that don't go away for some reason even if I templatise it and/or add a destructor myself. The symbol it tries to link to changes in the latter case, but that's it. Currently trying to test why it happens.
Found the problem. It's this:
struct _DirIteratorTempl()
{
//dir iterator implementation...
}
alias DirIterator = _DirIteratorTempl!();
auto dirEntries()(string path, SpanMode mode, bool followSymlink = true)
{
//this works, no linker errors
return _DirIteratorTempl!()(path, mode, followSymlink);
}
auto dirEntries()(string path, SpanMode mode, bool followSymlink = true)
{
//this gives linker errors just as if _DirIteratorTempl wasn't a template at all.
return DirIterator(path, mode, followSymlink);
}
We're lucky that the DirIterator name is undocumented, so it should not be a problem that I break it by making it a template. Otherwise we would be out of luck, because I could not alias it so that it would work without -dip1000 or -i=std.
So it seems that BinaryHeap.front is supposed to be @safe, but isn't anymore because it relied on access to RefCounted payload being @safe. Sigh. And CircleCI is reporting a linking error in DRunTime. There should be no way in the world that changes to RefCounted could cause DRunTime to not link but OTOH we already saw it causing a DMD script to not link. Who knows.