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

Manually implement tempname on non-windows #38879

Merged
merged 5 commits into from
Dec 28, 2021
Merged

Manually implement tempname on non-windows #38879

merged 5 commits into from
Dec 28, 2021

Conversation

vchuravy
Copy link
Member

@vchuravy vchuravy commented Dec 14, 2020

Fixes #35785

base/file.jl Outdated Show resolved Hide resolved
base/file.jl Outdated Show resolved Hide resolved
@StefanKarpinski
Copy link
Member

Shouldn't this do the opposite? If parent is passed, TMPDIR is ignored? If someone has explicitly asked for a temporary path to be in a certain directory, then we should respect that. They might do this, for example, if they need to ensure that it is on the same file system as a final location that they need to be able to move it to without copying. With this change, setting TMPDIR would wreck all attempts to do that.

@vchuravy
Copy link
Member Author

Shouldn't this do the opposite? If parent is passed, TMPDIR is ignored? If someone has explicitly asked for a temporary path to be in a certain directory, then we should respect that. They might do this, for example, if they need to ensure that it is on the same file system as a final location that they need to be able to move it to without copying. With this change, setting TMPDIR would wreck all attempts to do that.

I am not sure I understand. This is doing exactly what you are describing.

If the user passes parent we will use that as the directory to prefix the name with, we ensure that libc honors the passed parent directory by unsetting TMPDIR since otherwise it would ignore parent and use TMPDIR instead. Since we use parent=tempdir() in the case that parent is not passed we still honor TMPDIR as an environment variable.

We could also achieve the same by setting TMPDIR to parent, and could then pass an empty string to libc.

@StefanKarpinski
Copy link
Member

The comment says:

If TMPDIR is set tempname will ignore parent as an argument (#38873)

That's the opposite of what it should do: if parent is passed, ignore TMPDIR.

@vchuravy
Copy link
Member Author

Currently:

vchuravy@odin ~> TMPDIR=~ julia
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.5.3 (2020-11-09)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> tempname()
"/home/vchuravy/jl_Z9se0A"

julia> tempname("/opt")
"/home/vchuravy/jl_hI1x9x"

With this PR:

vchuravy@odin ~/s/julia (vc/tempname)> TMPDIR=~ julia
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.5.3 (2020-11-09)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> @eval Base.Filesystem function tempname(parent::AbstractString=tempdir(); cleanup::Bool=true)
                  isdir(parent) || throw(ArgumentError("$(repr(parent)) is not a directory"))
                  # If TEMPDIR is set tempname will ignore `parent` as an argument (#38873)
                  s = withenv("TMPDIR" => nothing) do
                      p = ccall(:tempnam, Cstring, (Cstring, Cstring), parent, temp_prefix)
                      systemerror(:tempnam, p == C_NULL)
                      s = unsafe_string(p)
                      Libc.free(p)
                      return s
                  end
                  cleanup && temp_cleanup_later(s)
                  return s
              end
tempname (generic function with 2 methods)

julia> tempname()
"/home/vchuravy/jl_u70Ddi"

julia> tempname("/opt")
"/opt/jl_WNbezj"

@vchuravy
Copy link
Member Author

The comment says:

If TMPDIR is set tempname will ignore parent as an argument (#38873)

That's the opposite of what it should do: if parent is passed, ignore TMPDIR.

Ah! I see where the confusion comes from. I was documenting the behavior of :tempnam and not the behavior of the Julia function tempname.

@StefanKarpinski
Copy link
Member

Ah, I understand the confusion: the comment is explaining why we need to unset TMPDIR whereas I read it as explaining how the function behaves. Perhaps there's some clearer way of wording that? Something like this:

Unless we unset TMPDIR here, it will cause tempnam to give a directory in that location, which would contradict the parent argument passed in here.

I worry about the thread safety of messing with TMPDIR like this. What if, for example, two different threads do this at the same time?

@vchuravy
Copy link
Member Author

I worry about the thread safety of messing with TMPDIR like this. What if, for example, two different threads do this at the same time?

Yeah... setenv is not thread-safe at all.

@StefanKarpinski
Copy link
Member

We could at least put a lock around this so that multiple calls to this function from different threads don't end up concurrently modifying the same environment variable.

@DilumAluthge
Copy link
Member

Instead of setenv, would withenv work? Would that be thread-safe?

@vchuravy
Copy link
Member Author

Instead of setenv, would withenv work? Would that be thread-safe?

I am using withenv here, but that uses setenv internally.

@DilumAluthge
Copy link
Member

Instead of setenv, would withenv work? Would that be thread-safe?

I am using withenv here, but that uses setenv internally.

Ah fair enough

@staticfloat
Copy link
Member

The issue is not just Julia code; if any C library (such as, for instance, tempname()) tries to read an environment variable while another C library (or some Julia task) modifies the environment block, we can segfault. We don't run into this very often because we tend to set environment variables at the beginning of program execution and leave them from then on, but the more we use withenv() the more likely we are to create this problem. To be explicit, a Julia mutex won't do anything but ensure that Julia can't interfere with other Julia operations. That's not even as helpful as you might think however, because since tempnam() uses getenv() to read the contents of $TMPDIR, if you have two Julia tasks that each wait for a mutex to call setenv() or getenv(), you can still have Julia acquire the (julia) mutex before calling setenv() and have that occur at a bad point in time for some other Julia task which is calling tempnam().

Possible solutions:

  • We could surround all ccall()'s with acquiring a mutex and not release it until the ccall() returns. Ha ha, right. This at least avoids crashes being Julia's fault; C code could still start a thread that calls setenv() within it at some point and causes a segfault.

  • We can hijack getenv() and setenv() somehow and force these functions to become threadsafe. A custom-build libc, symbol injection, etc... This is the only truly foolproof method that I know of, and will impose a performance penalty on environment actions, but IMO that's totally acceptable.

  • We can avoid modification of environment variables at unsafe points (where "unsafe" means after any thread has been started by anything). Realistically, this means you should set environment variables in things like __init__() and leave it at that. I will note that I am guilty as anyone in that JLL wrappers set environment variables when launching external processes, but I've been working to eliminate that by passing an environment block to open(::Cmd) rather than using withenv, and in so doing avoiding setenv() on the parent process altogether.

Keno
Keno previously requested changes Dec 25, 2020
Copy link
Member

@Keno Keno left a comment

Choose a reason for hiding this comment

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

Per discussion, should avoid using libc, since messing with the environment is inherently problematic.

@vchuravy vchuravy changed the title Unset TMPDIR for tempname Manually implement tempname on non-windows Jan 8, 2021
base/file.jl Outdated
@@ -578,21 +578,40 @@ end

else # !windows

# Based of musl __randname
function _rand_string()
r = Base.rand(UInt) # FIXME: https://git.musl-libc.org/cgit/musl/tree/src/temp/__randname.c
Copy link
Member Author

Choose a reason for hiding this comment

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

Any good ideas on how to get a random number here? Musl use time_ns, but then mixes in two pointers as sources of randomness.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a particular reason we are avoiding Base.rand ?

Copy link
Member

Choose a reason for hiding this comment

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

Even if we use Base.rand, we might want to initialize our own seed since we don't want the caller to be able to override this, no?

Copy link
Member Author

Choose a reason for hiding this comment

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

since we don't want the caller to be able to override this, no?

I don't think we care about the state of the rng. IIUC we just want a random seed that is not likely to be used by another process. This is why musl mixes in the two pointers to get some randomness in the higher bits, but we don't need to high quality randomness.

Is there a particular reason we are avoiding Base.rand ?

Base.rand is implemented through the stdlib Random and thus we would add a hard-dependency edge. Otherwise I would just use Random.randstring.

Copy link
Member

Choose a reason for hiding this comment

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

I believe Libc.rand() should provide suitable quality for this. It appears we can also use the same code everywhere now, rather than using separate, identical implementation for windows.

base/file.jl Outdated
end
String(buf)
end

# Obtain a temporary filename.
function tempname(parent::AbstractString=tempdir(); cleanup::Bool=true)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should use this implementation on Windows as well

@musm
Copy link
Contributor

musm commented Jan 8, 2021

I find it a bit odd we are trying to implement tempnam since the first sentence of the description section in http://man7.org/linux/man-pages/man3/tempnam.3.html says "Never use this function." ... use mkstemp instead.

@vchuravy
Copy link
Member Author

vchuravy commented Jan 8, 2021

I find it a bit odd we are trying to implement tempnam since the first sentence of the description section in http://man7.org/linux/man-pages/man3/tempnam.3.html says "Never use this function." ... use mkstemp instead.

🤷 I read that as well and found it entertaining, I assume they want to discourage usage since there is the risk of running into collisions and mkstemp will lock the file immediatly.

@StefanKarpinski
Copy link
Member

OMG, can we finish this?

@vchuravy
Copy link
Member Author

vchuravy commented May 8, 2021

OMG, can we finish this?

xD If anyone want to pick this up, please go ahead.

@staticfloat
Copy link
Member

Rebased and unified a bit, as requested by reviewers.

base/file.jl Outdated Show resolved Hide resolved
staticfloat and others added 2 commits December 16, 2021 16:59
This was a confusingly-named function, since `tempname()` doesn't create
a file (and is thus prone to race conditions) whereas `mkstemp()`
creates a file, reserving that filename as used immediately.
@musm
Copy link
Contributor

musm commented Dec 17, 2021

Any reason not to use this also on Windows?

@staticfloat
Copy link
Member

Any reason not to use this also on Windows?

Despite the PR title, this does unify much of the windows/posix divide.

@staticfloat
Copy link
Member

This is green, and I believe all reviewer concerns have been addressed. I'm going to add the merge me label.

@staticfloat staticfloat added merge me PR is reviewed. Merge when all tests are passing and removed help wanted Indicates that a maintainer wants help on an issue or pull request labels Dec 22, 2021
@vchuravy
Copy link
Member Author

Thanks Elliot for picking this up!

@DilumAluthge
Copy link
Member

Thank you Valentin and Elliot!

@DilumAluthge DilumAluthge merged commit 693b447 into master Dec 28, 2021
@DilumAluthge DilumAluthge deleted the vc/tempname branch December 28, 2021 10:36
@DilumAluthge DilumAluthge removed the merge me PR is reviewed. Merge when all tests are passing label Dec 28, 2021
@Keno
Copy link
Member

Keno commented Dec 29, 2021

@Keno
Copy link
Member

Keno commented Dec 29, 2021

I will note that the file name is the same in both of these failures, so I'm assuming the RNG doesn't get seeded during precompile generation and since we're using tempname here, we probably end up clashing somewhere.

Edit: I may be too tired or I linked the wrong reports.

Keno added a commit that referenced this pull request Dec 29, 2021
See comments at the end of #38879
@Keno
Copy link
Member

Keno commented Dec 29, 2021

We may want to revert this until #43597 is merged. This is likely quite destabilizing to CI.

Keno added a commit that referenced this pull request Dec 30, 2021
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Feb 22, 2022
Co-authored-by: Steven G. Johnson <[email protected]>

Co-authored-by: Elliot Saba <[email protected]>

Co-authored-by: Steven G. Johnson <[email protected]>
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Feb 22, 2022
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Mar 8, 2022
Co-authored-by: Steven G. Johnson <[email protected]>

Co-authored-by: Elliot Saba <[email protected]>

Co-authored-by: Steven G. Johnson <[email protected]>
LilithHafner pushed a commit to LilithHafner/julia that referenced this pull request Mar 8, 2022
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.

tmpname(parent) does not respect parent
8 participants