Thread overview | |||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
May 26, 2020 @trusted assumptions about @safe code | ||||
---|---|---|---|---|
| ||||
Deep in the discussion thread for DIP 1028 there is this little remark by Zoadian [1]: > you can break previously verified @trusted code by just writing @safe code today. That statement fits something that occurred to me when trying to lock down the definition of "safe interfaces" [2]. Consider this little program that prints the address and first character of a string in a convoluted way: import std.stdio; char f(string s) @trusted { immutable(char)* c = s.ptr; writeln(g(* cast(size_t*) &c)); return *c; } size_t g(ref size_t s) @safe { return s; } void main() @safe { writeln(f("foo")); } As the spec stands, I believe it allows f to be @trusted. The function doesn't exhibit undefined behavior, and it doesn't leak any unsafe values or unsafe aliasing. So it has a safe interface and can be @trusted. With f correctly @trusted and everything else being @safe, that code is good to go safety-wise. Now imagine that some time passes. Code gets added and shuffled around. Maybe g ends up in another module, far away from f. And as it happens, someone adds a line to g: size_t g(ref size_t s) @safe { s = 0xDEADBEEF; /* ! */ return s; } g is still perfectly @safe, and there's not even any @trusted code in the vicinity to consider. So review comes to the conclusion that the change is fine safety-wise. But g violates an assumption in f. And with that broken assumption, memory safety comes crumbling down, "by just writing @safe code". I think that's a problem. Ideally, it should not be possible to cause memory corruption by adding a line to @safe code. But I know that I'm more nitpicky than many when it comes to that rule and @safe/@trusted in general. Anyway, one way to address this would be disallowing f's call to g. I.e., add a sentence like this to the spec: Undefined behavior: Calling a safe function or a trusted function with unsafe values or unsafe aliasing has undefined behavior. The aliasing of `immutable(char)*` and `size_t` is unsafe, so the call becomes invalid. That means f can no longer be @trusted as it it's now considered to have undefined behavior. The downside is that functions may become invalid even when they don't make bad assumptions. For example, this f2 would (arguably?) also be invalid, because the unsafe aliasing is still there even though it's not being used: char f2(string s) @trusted { immutable(char)* c = s.ptr; writeln(g(* cast(size_t*) &c)); return 'x'; /* ! */ } Or maybe we say that c gets invalidated by the call to g and using it afterwards triggers undefined behavior. Then f2 is ok. Would still have to disallow calls that pass both ends of an unsafe aliasing to an @safe/@trusted function, though. Thoughts? Am I overthinking it as usual when it comes to @trusted? [1] https://forum.dlang.org/post/iwddwsdpsntajyblnttk@forum.dlang.org [2] https://dlang.org/spec/function.html#safe-interfaces |
May 25, 2020 Re: @trusted assumptions about @safe code | ||||
---|---|---|---|---|
| ||||
Posted in reply to ag0aep6g | On Monday, 25 May 2020 at 23:04:49 UTC, ag0aep6g wrote: > Deep in the discussion thread for DIP 1028 there is this little remark by Zoadian [1]: > >> you can break previously verified @trusted code by just writing @safe code today. > > That statement fits something that occurred to me when trying to lock down the definition of "safe interfaces" [2]. > > Consider this little program that prints the address and first character of a string in a convoluted way: > > import std.stdio; > char f(string s) @trusted > { > immutable(char)* c = s.ptr; > writeln(g(* cast(size_t*) &c)); > return *c; > } > size_t g(ref size_t s) @safe > { > return s; > } > void main() @safe > { > writeln(f("foo")); > } > > As the spec stands, I believe it allows f to be @trusted. The function doesn't exhibit undefined behavior, and it doesn't leak any unsafe values or unsafe aliasing. So it has a safe interface and can be @trusted. > > With f correctly @trusted and everything else being @safe, that code is good to go safety-wise. My reading of the spec is that f violates this requirement of safe interfaces: > 3. it cannot introduce unsafe aliasing that is accessible from other parts of the program. "Other parts of the program," taken at face value, should include both f's callers (direct and indirect) as well as any functions it calls (directly or indirectly). Since f introduces unsafe aliasing, and makes that aliasing visible to g, it should not be marked as @trusted. I suppose it depends on exactly what is meant by "accessible"--if it refers to the aliased memory location, then my interpretation follows, but if it refers to the pointers, there's an argument to be made that f is fine, since only one of the pointers escapes. |
May 25, 2020 Re: @trusted assumptions about @safe code | ||||
---|---|---|---|---|
| ||||
Posted in reply to ag0aep6g | On Monday, 25 May 2020 at 23:04:49 UTC, ag0aep6g wrote: > Deep in the discussion thread for DIP 1028 there is this little remark by Zoadian [1]: > >> you can break previously verified @trusted code by just writing @safe code today. > > That statement fits something that occurred to me when trying to lock down the definition of "safe interfaces" [2]. > > Consider this little program that prints the address and first character of a string in a convoluted way: > > import std.stdio; > char f(string s) @trusted > { > immutable(char)* c = s.ptr; > writeln(g(* cast(size_t*) &c)); > return *c; > } > size_t g(ref size_t s) @safe > { > return s; > } > void main() @safe > { > writeln(f("foo")); > } > > As the spec stands, I believe it allows f to be @trusted. The function doesn't exhibit undefined behavior, and it doesn't leak any unsafe values or unsafe aliasing. So it has a safe interface and can be @trusted. > > With f correctly @trusted and everything else being @safe, that code is good to go safety-wise. No, `f` should be just dead `@system`. If you call f with a string which does not point to null, but is empty, boom! But for rest of the reply, I assume you meant a function that could really be `@trusted` with all parameters. > > Now imagine that some time passes. Code gets added and shuffled around. Maybe g ends up in another module, far away from f. And as it happens, someone adds a line to g: > > size_t g(ref size_t s) @safe > { > s = 0xDEADBEEF; /* ! */ > return s; > } Credible scenario. > > g is still perfectly @safe, and there's not even any @trusted code in the vicinity to consider. So review comes to the conclusion that the change is fine safety-wise. But g violates an assumption in f. And with that broken assumption, memory safety comes crumbling down, "by just writing @safe code". > > I think that's a problem. Ideally, it should not be possible to cause memory corruption by adding a line to @safe code. But I know that I'm more nitpicky than many when it comes to that rule and @safe/@trusted in general. I don't think it as a problem for `@safe`. `@safe` is just a command to turn the memory checking tool on, not a code certification (although using @safe where possible would probably be required for certifying). Combating the scenarios you mentioned means that the `@safe` function called must be at least as certified as the `@trusted` caller, but that is no reason to forbid using the memory checking tool the language offers. |
May 26, 2020 Re: @trusted assumptions about @safe code | ||||
---|---|---|---|---|
| ||||
Posted in reply to Paul Backus | On 26.05.20 01:25, Paul Backus wrote:
> My reading of the spec is that f violates this requirement of safe interfaces:
>
>> 3. it cannot introduce unsafe aliasing that is accessible from other parts of the program.
>
> "Other parts of the program," taken at face value, should include both f's callers (direct and indirect) as well as any functions it calls (directly or indirectly). Since f introduces unsafe aliasing, and makes that aliasing visible to g, it should not be marked as @trusted.
>
> I suppose it depends on exactly what is meant by "accessible"--if it refers to the aliased memory location, then my interpretation follows, but if it refers to the pointers, there's an argument to be made that f is fine, since only one of the pointers escapes.
Hm. The meaning I intended with that is that it's only invalid when the memory location becomes accessible via both types elsewhere. And g only has access via one type.
Would you say that this next function is also leaking unsafe aliasing?
immutable(int[]) f() @trusted
{
int[] a = [1, 2, 3];
a[] += 10;
return cast(immutable) a;
}
Because that one is definitely supposed to be allowed.
And I must also say that I didn't really consider called functions to be "other parts of the program". But reading it that way makes sense. Then I suppose calling an @safe function with both ends of an unsafe aliasing can be seen as already not allowed.
|
May 26, 2020 Re: @trusted assumptions about @safe code | ||||
---|---|---|---|---|
| ||||
Posted in reply to Dukc | On 26.05.20 01:47, Dukc wrote: > No, `f` should be just dead `@system`. If you call f with a string which does not point to null, but is empty, boom! Right. I can fix that by changing immutable(char)* c = s.ptr; to immutable(char)* c = &s[0]; correct? [...] > I don't think it as a problem for `@safe`. `@safe` is just a command to turn the memory checking tool on, not a code certification (although using @safe where possible would probably be required for certifying). And you don't think it's possible and worthwhile to strengthen @safe so far that it becomes a code certification? > Combating the scenarios you mentioned means that the `@safe` function called must be at least as certified as the `@trusted` caller, but that is no reason to forbid using the memory checking tool the language offers. In my scenario, the @safe function is (supposed to be) perfectly safe before and after the catastrophic change. You can try certifying it beyond what the compiler does, but there's really nothing wrong with the function. The thing is that editing the @safe function affects the status of the @trusted one. And I think it should be possible to tweak the rules so that a correctly verified @trusted function cannot become invalid when something changes in a called @safe function. |
May 26, 2020 Re: @trusted assumptions about @safe code | ||||
---|---|---|---|---|
| ||||
Posted in reply to ag0aep6g | On Tuesday, 26 May 2020 at 00:01:37 UTC, ag0aep6g wrote:
> Hm. The meaning I intended with that is that it's only invalid when the memory location becomes accessible via both types elsewhere. And g only has access via one type.
>
> Would you say that this next function is also leaking unsafe aliasing?
>
> immutable(int[]) f() @trusted
> {
> int[] a = [1, 2, 3];
> a[] += 10;
> return cast(immutable) a;
> }
>
> Because that one is definitely supposed to be allowed.
There's no part of the program outside of f's body that has access to either reference while both are alive, so I'd say that's fine.
In Rust-ish terms, the ownership of the array is being moved from a to the return value, whereas in the previous example, both f and g were attempting to mutably borrow the same data at the same time.
|
May 26, 2020 Re: @trusted assumptions about @safe code | ||||
---|---|---|---|---|
| ||||
Posted in reply to ag0aep6g | On Monday, 25 May 2020 at 23:04:49 UTC, ag0aep6g wrote:
> Deep in the discussion thread for DIP 1028 there is this little remark by Zoadian [1]:
>
>> you can break previously verified @trusted code by just writing @safe code today.
>
> That statement fits something that occurred to me when trying to lock down the definition of "safe interfaces" [2].
>
> Consider this little program that prints the address and first character of a string in a convoluted way:
>
> import std.stdio;
> char f(string s) @trusted
> {
> immutable(char)* c = s.ptr;
> writeln(g(* cast(size_t*) &c));
> return *c;
> }
> size_t g(ref size_t s) @safe
> {
> return s;
> }
> void main() @safe
> {
> writeln(f("foo"));
> }
You are passing a pointer into a function that takes a mutable size_t by reference and then use the pointer afterwards. You get what's coming to you if you think that's suitable for @trusted.
This is a good example that care must still be taken in @trusted. You are doing something dangerous, expect to be burned by it.
char f(string s) @trusted
{
{
immutable(char)* c = s.ptrl
writeln(g(* cast(size_t*) &c));
// var c invalidated by above function, don't use after this line
}
return s[0];
}
|
May 26, 2020 Re: @trusted assumptions about @safe code | ||||
---|---|---|---|---|
| ||||
Posted in reply to Arine | On Tuesday, 26 May 2020 at 00:57:52 UTC, Arine wrote:
> char f(string s) @trusted
> {
> {
> immutable(char)* c = s.ptrl
> writeln(g(* cast(size_t*) &c));
> // var c invalidated by above function, don't use after this line
> }
> return s[0];
> }
Ops, even that would still need a size check as well.
|
May 26, 2020 Re: @trusted assumptions about @safe code | ||||
---|---|---|---|---|
| ||||
Posted in reply to Arine | On 26.05.20 02:57, Arine wrote:
> You are passing a pointer into a function that takes a mutable size_t by reference and then use the pointer afterwards. You get what's coming to you if you think that's suitable for @trusted.
>
> This is a good example that care must still be taken in @trusted. You are doing something dangerous, expect to be burned by it.
So would you say that the function should not have been @trusted in the first place, because it can't guarantee to stay safe?
Or was the @trusted attribute okay at first, and it only became invalid later when the @safe code changed? And is it acceptable that @safe code can invalidate @trusted attributes like that?
|
May 26, 2020 Re: @trusted assumptions about @safe code | ||||
---|---|---|---|---|
| ||||
Posted in reply to ag0aep6g | On 26.05.20 01:04, ag0aep6g wrote:
> Consider this little program that prints the address and first character of a string in a convoluted way:
>
> import std.stdio;
> char f(string s) @trusted
> {
> immutable(char)* c = s.ptr;
> writeln(g(* cast(size_t*) &c));
> return *c;
> }
> size_t g(ref size_t s) @safe
> {
> return s;
> }
> void main() @safe
> {
> writeln(f("foo"));
> }
>
> As the spec stands, I believe it allows f to be @trusted.
I don't think so. @trusted code can't rely on @safe code behaving a certain way to ensure memory safety, it has to be defensive.
|
Copyright © 1999-2021 by the D Language Foundation