May 26, 2020
On Monday, 25 May 2020 at 23:04:49 UTC, 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. 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.
>

As mentioned by others, it is incorrect to label `f` with `@trusted` because:
1. It provides unsafe access to potentially out of bounds memory - there's no guarantee that `s.length >= size_t.sizeof` is true (in addition to the possibility of `s.ptr` being `null`).
2. It creates mutable aliasing to immutable memory and passes it to another function. Casting away `immutable` could be safe iff the mutable reference can't be used to modify the memory. So, `g` must take a `const` reference to `size_t`, in order for `f` to even begin to be considered a candidate for the `@trusted` attribute.
May 26, 2020
On 5/26/2020 12:07 AM, Timon Gehr wrote:
> 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.

I agree. The trusted code here is not passing safe arguments to g(), but it is trusted to do so.
May 26, 2020
On Tuesday, 26 May 2020 at 00:22:05 UTC, ag0aep6g wrote:
> 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?
>
> [...]

Looks good to me. Well, not sure about that mutable aliasing thing others have mentioned, maybe it's good, maybe not.

>> 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?
>

No, even if @safe would in all instances be 100% idiot-proof memory safe, I think it would work badly as a code certificate. Why? It would mean that if you have an already `@safe` function that would be fit for use in `@system` code, you would have to either duplicate it or disable it's memory checks, because it could not be certified for `@system` code.

The closest thing to a certificate a code can have AFAIK is an automatic test suite. `@safe` works well in conjunction with such a test suite: if a function is both `@safe` and `pure`, you know that regular `unittest`s are enough to certify it in the general case. With `@system` or `@trusted`, Valgrinding might be required.

This also applies to your example: Had you tested that `g` does not mutate the pointed value (or set the parameter as `const`), the bug would have been caught. On the other hand, no amount of unit testing can alone certify `f` to a high standard, as it could still silently be corrupting the memory despite outputting and returning correctly


May 26, 2020
On 26.05.20 11:13, Petar Kirov [ZombineDev] wrote:
> On Monday, 25 May 2020 at 23:04:49 UTC, ag0aep6g wrote:
[...]
>>     char f(string s) @trusted
>>     {
>>         immutable(char)* c = s.ptr;
>>         writeln(g(* cast(size_t*) &c));
>>         return *c;
>>     }
[...]
> As mentioned by others, it is incorrect to label `f` with `@trusted` because:
> 1. It provides unsafe access to potentially out of bounds memory - there's no guarantee that `s.length >= size_t.sizeof` is true (in addition to the possibility of `s.ptr` being `null`).

I think you're misreading the code. It's not reading size_t.sizeof bytes starting at c. It's reinterpreting the pointer c itself as a size_t.

> 2. It creates mutable aliasing to immutable memory and passes it to another function. Casting away `immutable` could be safe iff the mutable reference can't be used to modify the memory. So, `g` must take a `const` reference to `size_t`, in order for `f` to even begin to be considered a candidate for the `@trusted` attribute.

Great. So far the majority opinion seems to be that f is invalid from the start, and calling g like that just can't considered be safe. I like it.
May 26, 2020
On 26.05.20 11:35, Walter Bright wrote:
> On 5/26/2020 12:07 AM, Timon Gehr wrote:
>> 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.
> 
> I agree. The trusted code here is not passing safe arguments to g(), but it is trusted to do so.

Nice. Timon and Walter agree on something related to safety. That must mean something.

I take it you guys are good with adding the note about undefined behavior to the spec then? Repeating it here for reference:

    Undefined behavior: Calling a safe function or a trusted
    function with unsafe values or unsafe aliasing has undefined
    behavior.
May 26, 2020
On 5/25/20 8:22 PM, ag0aep6g wrote:
> 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?

Yes, I was going to point that out too. But it doesn't affect the main point.

The main problem is that f does not provide a safe value to g. @safe code can always mess up if handed garbage. One might say that f should not be @trusted. But that just means NO functions could be trusted. If you cannot trust the semantic expectations of the functions you are calling to hold true, then you cannot write @trusted code ever. I mean, someone could do this inside memcpy:

size_t memcpy(void *dst, void *src, size_t length)
{
   *(size_t *)(dst + length + 10) = 0xdeadbeef;
   ...// normal implementation
}

And violate safety that way. So does that mean you can never use memcpy inside @trusted functions *just in case* it did something like this?

I get what you are saying, that it would be nice if one can just write @safe code and never worry that you might violate any memory rules. But in reality, you still have to implement the function as designed, and unless you do that, you are not going to be memory safe, period. The only call stacks that would be "safe" were ones that were @safe all the way down. And then @safe becomes essentially useless.

-Steve
May 26, 2020
On Tuesday, 26 May 2020 at 13:19:04 UTC, ag0aep6g wrote:
> I take it you guys are good with adding the note about undefined behavior to the spec then? Repeating it here for reference:
>
>     Undefined behavior: Calling a safe function or a trusted
>     function with unsafe values or unsafe aliasing has undefined
>     behavior.

As far as I can tell that's already implied by the first sentence under "safe interfaces":

> Given that it is only called with safe values and safe aliasing, a function has a safe interface when:

...but being more explicit seems like it can't hurt.

The clarification I'd add would be to modify requirement #3 of "Safe Interfaces" to read as follows:

> 3. it cannot introduce unsafe aliasing **of memory** that is accessible from other parts of the program **while that aliasing exists**.


May 26, 2020
On 26.05.20 15:19, ag0aep6g wrote:
> On 26.05.20 11:35, Walter Bright wrote:
>> On 5/26/2020 12:07 AM, Timon Gehr wrote:
>>> 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.
>>
>> I agree. The trusted code here is not passing safe arguments to g(), but it is trusted to do so.
> 
> Nice. Timon and Walter agree on something related to safety.

(We agree on many things. It just does not seem that way because I seldomly get involved when I agree with a decision.)
May 27, 2020
On 26.05.20 17:41, Steven Schveighoffer wrote:
> The main problem is that f does not provide a safe value to g. @safe code can always mess up if handed garbage. One might say that f should not be @trusted. But that just means NO functions could be trusted. If you cannot trust the semantic expectations of the functions you are calling to hold true, then you cannot write @trusted code ever. I mean, someone could do this inside memcpy:
> 
> size_t memcpy(void *dst, void *src, size_t length)
> {
>     *(size_t *)(dst + length + 10) = 0xdeadbeef;
>     ...// normal implementation
> }
> 
> And violate safety that way. So does that mean you can never use memcpy inside @trusted functions *just in case* it did something like this?
> 
> I get what you are saying, that it would be nice if one can just write @safe code and never worry that you might violate any memory rules. But in reality, you still have to implement the function as designed, and unless you do that, you are not going to be memory safe, period. The only call stacks that would be "safe" were ones that were @safe all the way down. And then @safe becomes essentially useless.

I think you've got a good point, but the example isn't so great. memcpy is @system and can only be @system, so of course you can break safety by changing its behavior.

But yeah, the @trusted function might rely on the @safe function returning 42. And when it suddenly returns 43, all hell breaks loose. There doesn't need to be any monkey business with unsafe aliasing or such. Just an @safe function returning an unexpected value.

I suppose the only ways to catch that kind of thing would be to forbid calling @safe (and other @trusted?) functions from @trusted (and @system?) code, or to mandate that the exact behavior of @safe functions (including their return values) cannot be relied upon for safety. Those would be really, really tough sells.

So unless we do something very drastic, any visible change in the behavior of an @safe function can possibly lead to memory corruption. And strictly speaking, any @trusted and @system code that calls it must be re-evaluated for safety.

Seems kinda obvious now. But I don't think I really realized this before.
May 27, 2020
On Tuesday, 26 May 2020 at 22:52:09 UTC, ag0aep6g wrote:
> But yeah, the @trusted function might rely on the @safe function returning 42. And when it suddenly returns 43, all hell breaks loose. There doesn't need to be any monkey business with unsafe aliasing or such. Just an @safe function returning an unexpected value.
>
> I suppose the only ways to catch that kind of thing would be to forbid calling @safe (and other @trusted?) functions from @trusted (and @system?) code, or to mandate that the exact behavior of @safe functions (including their return values) cannot be relied upon for safety. Those would be really, really tough sells.

All that's necessary is to have the @trusted function check that the assumption it's relying on is actually true:

@safe int foo() { ... }

@trusted void bar() {
    int fooResult = foo();
    assert(fooResult == 42);
    // proceed accordingly
}

If the assumption is violated, the program will crash at runtime rather than potentially corrupt memory.