Umask - set default file permissions

Hey all, first post here on discourse :wave: !

I had a use-case to manipulate private, information-sensitive files in Racket, didn't find a way to set the umask so new directories/files have secure permissions (race-condition free (!)) so I made a small package to fill this need.

It's a little rough around the edges โ€” for example I don't believe this works on Mac. Any suggestions appreciated :slight_smile: !

Project infos

Quick example

Same a SSH private key in your environment to a temporary file, return the path.

(with-umask #o077
  (let ([file (make-temporary-file)])
    (with-output-to-file file (thunk (write-string (getenv "APP_SSH_PRIVKEY")))
     #:exists 'must-truncate)
    file))

Get the umask

(umask)  ; -> #o022

Set the umask

(umask #o077)  ; -> void

(Note setting the umask does not return the previous umask - this is intentional to simplify the API use.)

5 Likes

Neat!

Without peaking, I'm assuming umask is a parameter, so with-umask is just a wrapper around parameterize? If so, I think it's completely normal for (umask x) to be void?.

1 Like

It feels like a parameter eh :slight_smile: ?

Unfortunately it's not a parameter in this case because I wasn't sure how to configure the parameter to have a side-effect when set - it'll have to do a FFI call whenever the parameter is changed.

Instead with-umask wraps the body with a few umask FFI calls around it. Maybe there's a better way to do this? :thinking:

2 Likes

A parameter can have a guard that produces side effects.

See doc on make-parameter.

Jos

1 Like

Jos is correct; you could do something like

(define umask (make-parameter default
                              (lambda (new-mask) side-effectsโ€ฆ new-mask)
                              'umask)

You can add a wrapper on the return value with make-derived-parameter.

1 Like

I'd be interested to know whether this work-in-progress PR to add a #:permissions argument to make-temporary-file would work for your use-case: https://github.com/racket/racket/pull/4126

You also raise a good point that the docs don't specify how with-output-to-file, open-output-port, etc. handle #:permissions when #:exists is something like 'must-truncate

1 Like

I gave it a go, but the tests appear to indicate my approach with parameters isn't quite working as expected, any hints?

I added dynamic-wind on the other hand, that appears to work in more cases. It does not work across threads however (though this didn't appear to work with parameters, possibly due to the FFI nature of the umask syscall).

It seems the guard is not applied when parameterize restores the old value, probably because that's not actually how parameterize works (something something thread cells something something ?).

I'm not sure there is a good workaround, esp. since, as you say, this is FFI-bound.

1 Like

I think the intent for the guard is to prevent an unwanted value to be used as the parameter value, so it seems sensible that guard is only used once to check that the value is allowed.
Mutable values or side-effects don't seem to play nicely with that.

For opengl side-effect management I have used dynamic-wind (making setup/teardown calls).
(That opengl code is single threaded, so didn't run into any multi-threading issues)

But I am not entirely sure about the differences between dynamic wind and parameters, I get the first is based on continuations, while the latter is based on thread cells and parameterize (but indirectly also continuations?).
My technical understanding of how parameters / dynamic-wind are implemented is rusty, comparing the two could be interesting. Currently it seems to me like I would need to dig into both their implementations to understand fully in what ways they differ or are similar in their use.

1 Like

I believe the intent of a parameter is to restrict dynamic-wind to a simple value binding, more or less. That is: a dynamic-wind can do any crazy thing it wants whenever control leaves or re-enters. A parameterize is a little more predictable; it just ensures that a particular binding is in place when the code in the dynamic extent of the body is being called.

In other words, if you need to perform an action on entry and a corresponding action on exit, I think you want dynamic-wind, and not parameterize.

However, I also think that @LiberalArtist 's proposal could be even better, and more well-behaved.

1 Like

One difference between parameterize and dynamic-wind is that parameterize will also set the initial state for child (Racket-level) threads: in other words, thread will capture a parameterization (but probably not call the guard when restoring the initial value), but will not capture winders.

(In addition to raising exception, a common use for a parameter guard is to do some kind of coercion, e.g. converting any non-#false value to #true.)

But I think the FFI is not going to be enough to make a really robust and general interface to umask. (That doesn't mean it's bad to have a less-general solution that works for some use case!) In particular, IIUC umask is set at the level of the OS process, so uses of with-umask in different Racket places will interfere with each other.

Some possible approaches I see:

  • Don't rely on state: explicitly supply #:permissions as needed.
  • Have rktio manage the OS-level umask.
  • Make a parameter current-umask that works like current-directory: it is entirely independent of the OS-level state, but Racket's IO primitives respect it.
3 Likes