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

Make std.stdio.File.readln @safe #8623

Merged
merged 18 commits into from
Dec 5, 2022
Merged

Make std.stdio.File.readln @safe #8623

merged 18 commits into from
Dec 5, 2022

Conversation

ntrel
Copy link
Contributor

@ntrel ntrel commented Nov 14, 2022

Make readlnImpl @trusted.
Make File.readln overloads @safe.

@dlang-bot
Copy link
Contributor

Thanks for your pull request and interest in making D better, @ntrel! 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: and Returns:)

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#8623"

@ntrel
Copy link
Contributor Author

ntrel commented Nov 14, 2022

Unrelated ae failure:
std.socket.SocketOSException@std/socket.d(2826): Unable to bind socket: Cannot assign requested address

@ntrel ntrel marked this pull request as ready for review November 14, 2022 18:24
@RazvanN7
Copy link
Collaborator

I'm a bit reluctant to trust such a large function. Are there many operations that prevent it from being @safe?

@ntrel
Copy link
Contributor Author

ntrel commented Nov 16, 2022

@RazvanN7 Only 34 ;-) I have made readlnImpl @safe now using @trusted lambdas.

std/stdio.d Outdated
{
alias trusted_FLOCK = (fps) @trusted => _FLOCK(fps);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a controversial pattern, because even though it's inside a function, it doesn't have a @safe interface and can cause memory corruption when called wrongly inside this @safe function body. (@aG0aep6G)

Without refactoring, the correct thing to do is mark the function @trusted. The @trusted lambda's may give the illusion that the surface of @trusted code is smaller, but you could still escape FILE* fps or add a wrong FLOCK/FUNLOCK call anywhere in the @safe body.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a controversial pattern, because even though it's inside a function, it doesn't have a @safe interface and can cause memory corruption when called wrongly inside this @safe function body. (@aG0aep6G)

I am pleased to see that my constant nagging is having an effect :)

Copy link
Member

Choose a reason for hiding this comment

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

One way to fix this is to encapsulate all the necessary trusted functionality into a static struct that is non-copyable, and use that instead.

Copy link
Member

Choose a reason for hiding this comment

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

Ideally that struct's data would be @system, but that's not in the language yet.

Copy link
Contributor Author

@ntrel ntrel Nov 17, 2022

Choose a reason for hiding this comment

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

@schveiguy It works with -preview=systemVariables (dlang/dmd#14478). I have implemented this. There is still one hole though, the user can call destroy on the struct instance. That will unlock even though an unshared file pointer may still be active (though access is protected with scope, the struct is still in scope).

Copy link
Member

Choose a reason for hiding this comment

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

The destructor should null the 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.

Why? destroy (or move) already does that.

Copy link
Member

Choose a reason for hiding this comment

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

OK, I misunderstood. I didn't realize you were exposing the pointer elsewhere.

@RazvanN7
Copy link
Collaborator

I'm sorry if I wasn't clear, but I wasn't asking to put all unsafe operations in trusted lambdas. I was just asking how prevalent are the unsafe patterns in the idea that maybe we could get rid of them.

Instead of wrong trusted_FUNLOCK.
Note: trusted_FGETC does have a @safe interface.
std/stdio.d Outdated
version (DIGITAL_MARS_STDIO)
{
auto lf = LockedFile(fps);
auto fp = lf.fp;
Copy link
Member

Choose a reason for hiding this comment

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

This defeats the purpose of the struct. The struct can't just be a wrapper for the type itself, it needs to be a wrapper for the functionality that should be trusted. So like instead of trusted_FGETWC, the struct should have a method on it that is trusted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, but I'm not sure how this defeats the purpose of the struct. What is actually unsafe if fp can only be accessed when the file is locked (aside from the destroy issue)?

Copy link
Member

Choose a reason for hiding this comment

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

FILE * fp;
{ auto lf = LockedFile(fps); fp = lf.fp; }

Copy link
Member

Choose a reason for hiding this comment

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

In other words, you should not be able to use the unlocked FILE * if the struct no longer exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(I plan to move trusted_FGETC to a struct method as that's better anyway).

lf.fp does not return a FILE*, it's a _iobuf*. On Windows I get:

Error: cannot implicitly convert expression `lf.fp()` of type `_iobuf*` to `shared(_iobuf)*`

But all the other code paths use a cast too so I assume they would trigger the same error. Aside from the type error, the assignment fp = lf.fp should fail due to -dip1000 but it doesn't:
https://forum.dlang.org/post/cvrzlycycprixbadbpgg@forum.dlang.org

Copy link
Member

@schveiguy schveiguy Nov 19, 2022

Choose a reason for hiding this comment

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

OK, I didn't know what exact type fp was returning, this wasn't my point though, I should have been more general in the example:

typeof(LockedFile.fp) fp;
{auto lf = LockedFile(fps); fp = lf.fp; }

The point is, if you just expose the pointer then it defeats the purpose of encapsulating the trusted behavior.

std/stdio.d Outdated
@disable void opAssign(LockedFile);

// Since fps is now locked, we can cast away shared
@trusted fp() return scope => cast(_iobuf*) fps;
Copy link
Member

Choose a reason for hiding this comment

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

This method should be @system

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've removed this method now. I assume you said that because return scope doesn't enforce that the result doesn't live longer than the struct. That seems to be a significant missing piece with -dip1000.

Copy link
Member

@schveiguy schveiguy Nov 19, 2022

Choose a reason for hiding this comment

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

I mean the point is to encapsulate the unlocked pointer away from usage.

I didn't realize you weren't actually storing the unlocked pointer, but using this to cast it. I was expecting the LockedFile to be storing the unlocked pointer, and protect it from incorrect usage.

std/stdio.d Outdated
{
auto lf = LockedFile(fps);
// Since fps is now locked, we can cast away shared
auto fp = (() @trusted => cast(_iobuf*) fps)();
Copy link
Member

Choose a reason for hiding this comment

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

this shouldn't be done at all, use the LockedFile. Just add any needed trusted methods to the struct.

If all trusted operations are encapsulated in the struct, then you don't have to worry about this function in terms of safety (you do right now, because @system data isn't yet a thing, but eventually).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just add any needed trusted methods to the struct

Well, they're fields:

stdio.d:5509: if (__fhnd_info[fp._file] & FHND_WCHAR)
stdio.d:5541: else if (fp._flag & _IONBF)
stdio.d:5565: int u = fp._cnt;
stdio.d:5566: char* p = fp._ptr;
stdio.d:5568: if (fp._flag & _IOTRAN)
stdio.d:5610: fp._cnt -= i;
stdio.d:5611: () @trusted { fp._ptr += i; }();

Maybe I could add an opDispatch.

If all trusted operations are encapsulated in the struct, then you don't have to worry about this function in terms of safety

I already need to call a trusted lambda to call free. I don't really see what's wrong with immediately calling a trusted lambda, the @trustedness is not being hidden. It's interface is safe at that point in the execution.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, I suppose if there's a lf.destroy call added then the fp is not safe.

Copy link
Member

Choose a reason for hiding this comment

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

Most of this does not need to happen on the structure itself, use locals (it's actually faster anyway to manipulate local pointers/counters than use an indirection, from my experience in iopipe). Also, if needed you can encapsulate the whole functionality into the struct itself, or make this section a trusted lambda.

Yeah, I'm looking at the code now, and actually I think this isn't getting the picture across, I'm sorry, trusted code is just so hard to get right. The opDispatch is incorrectly marked safe, because it's treating the pointer as a safe pointer, when it should be a system pointer.

When I get a few minutes, I'll rewrite what I think it should be.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@schveiguy I think I've got it right now, thanks for your help. I put all the digital mars stdio code in a single trusted lambda and did the same for the getdelim code too.

Copy link
Member

Choose a reason for hiding this comment

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

Looks a lot better! One thing I would recommend, store the actual pointer under a more obscure name, and access via a @system accessor, which will enable proper safety checking even without the @system data DIP implemented. Currently the simple access of the private member is still considered @safe by the compiler, and this will at least involve the enforcement, even if the requirement is artificial.

e.g.:

private struct LockedFile {
   @system private _iobuf* _fp; // obscure name to avoid accidental usage
   @system _iobuf* fp() { return _fp; } // use this in all @trusted calls
   ...
}

When the system variable DIP is default, this can be changed to just a field.

@dkorpel
Copy link
Contributor

dkorpel commented Dec 1, 2022

Is this ready to go?

@ntrel
Copy link
Contributor Author

ntrel commented Dec 4, 2022

@dkorpel Yes IMO. Steven recommended a workaround until @system variables are out of preview, but is that change needed?

#8623 (comment)

@dkorpel
Copy link
Contributor

dkorpel commented Dec 5, 2022

is that change needed?

I don't think so. It's internal / private anyway, and accessing system variables by default already outputs deprecation messages.

@dkorpel dkorpel merged commit 09275da into dlang:master Dec 5, 2022
@ntrel ntrel deleted the safe-readln branch December 5, 2022 12:37
@ntrel
Copy link
Contributor Author

ntrel commented Dec 10, 2022

Does anyone know if it's safe to make stdio stdout stderr @trusted? Then free function readln can be @safe as well as File.readln.

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.

7 participants