skip to content

Why does os.OpenFile with perm 0666 often create a file with mode 0644?

level: middleimportance: should knowfreq 50%

answer

  1. the number you pass is a request
  2. the environment subtracts, never adds
  3. a per-process mask, inherited, invisible in the code
  4. 0666 with the usual mask lands at 0644

basics

~20 s

The perm argument is only a request. The kernel clears the bits set in the process umask, so with the usual umask of 022 a requested 0666 becomes 0644. Go neither reads nor applies the umask itself.

solid answer

~50 s

`perm` in `os.OpenFile(name, flag, perm)` is the mode the file would get if nothing interfered, and something always does: on Unix the kernel removes every bit set in the calling process's umask, so a typical umask of `022` turns `0666` into `0644` and `0777` into `0755`. Go does not consult or modify the umask — it hands your bits to the operating system unchanged, which is why the same program produces different modes under a different service manager or shell. Two consequences matter in practice. A umask can only *clear* bits, never add them, so requesting `0600` for a file holding a secret is safe: it can come out `0600` or tighter, never looser. And `perm` is used only when the call actually creates the file — opening an existing path ignores it, so an already-permissive file stays permissive no matter what you pass.

code

go · 5 lines
go
// new path, process umask 022: the file lands as 0644
f, err := os.OpenFile("journal.log", os.O_CREATE|os.O_WRONLY, 0o666)

// path already exists as 0644: the 0600 here changes nothing at all
g, err := os.OpenFile("token.txt", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)

go deeper

for a junior

Know that the third argument of os.OpenFile is the mode for a file it creates, that it is written in octal, and that the file you get may end up with fewer permissions than you asked for.

for a middle

Explain the bit-clear against the process umask with a worked example, and state clearly that the argument is consulted only when the call creates the file.

for a senior

Show that you would pass a tight mode for anything sensitive rather than relying on the environment, and that you would create exclusively when you need to know the mode you requested is the mode in force.

for a principal

Own the policy question: whether file modes are set per call site or standardised by the service's runtime environment, and how that decision is verified rather than assumed across the platforms you deploy on.

### perm is an intention, not an outcome `os.OpenFile(name string, flag int, perm os.FileMode) (*os.File, error)` takes a mode as its third argument. It is tempting to read that as "the file will have this mode". It will not, in general. The value you pass is a *maximum* that the operating system then reduces. On Unix the reduction is the **umask**: a per-process mask of permission bits that the kernel clears from every newly created file and directory. The arithmetic is a bit-clear, written in Go's own notation as `perm &^ umask`: - umask `022`, request `0666` → `0644` (owner read-write, everyone else read-only) - umask `022`, request `0777` → `0755` - umask `077`, request `0666` → `0600` - umask `000`, request `0666` → `0666` The umask is inherited from whatever started the process — a login shell, a service manager, a CI runner, a container runtime — and it is not visible anywhere in the Go program. That is exactly why the same binary produces a `0644` file on a developer's laptop and something else in production. Nothing in the Go standard library reads it or sets it for you; the `os` package passes your bits straight to the `open` system call. ### The direction of the mask matters A umask can only turn bits **off**. This has a useful security consequence: if a file must not be readable by anyone but its owner, pass `0600` and stop worrying about the environment. A stricter umask can make the result tighter still, but no umask can make it `0644`. Conversely, no permission argument can make the result *more* permissive than what you asked for, so passing `0666` is not itself a vulnerability — it is a request that the environment is allowed to trim, and in an environment with umask `000` it really will produce a world-writable file. For anything sensitive, ask for what you actually want rather than the conventional `0666`. ### perm applies only at creation The second half of the surprise is that `perm` is consulted **only when the open call creates the file**. If the path already exists, the argument is ignored entirely and the file keeps whatever mode it already had. So this does not tighten anything: ```go // if secrets.json already exists as 0644, it stays 0644 f, err := os.OpenFile("secrets.json", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) ``` If you need a guarantee rather than a hope, create the file exclusively with `os.O_CREATE|os.O_EXCL` so you know the mode you asked for is the mode that was applied, and treat an already-existing path as a condition to handle rather than to overwrite. Changing the mode of a file that already exists is a separate operation with its own call, not something an open can do. ### Which bits of fs.FileMode are even looked at `os.FileMode` is an alias for `fs.FileMode`, a 32-bit value whose *low* nine bits are the familiar owner/group/other read-write-execute permissions, and whose *high* bits describe the kind of file — `fs.ModeDir`, `fs.ModeSymlink`, `fs.ModeNamedPipe`, `fs.ModeSocket`, `fs.ModeDevice` — plus the special bits `fs.ModeSetuid`, `fs.ModeSetgid` and `fs.ModeSticky`. When you pass a mode to `os.OpenFile`, only the nine permission bits and those three special bits are meaningful; the type bits are ignored, because an ordinary open cannot make a directory or a socket. `FileMode.Perm()` is the helper that masks a mode down to just those nine bits. Write the literal in octal — `0o644` in modern Go, `0644` in older code — because the bits are grouped in threes. A decimal `644` is a completely different and nonsensical mode. ### Portability On Windows there is no umask and no Unix permission model; the `os` package largely ignores the permission bits, with the write bit being the part that has an effect (a file requested without owner write shows up read-only). Code that relies on `0600` for confidentiality is therefore relying on a Unix guarantee, which is fine for a service that only runs on Unix but is worth stating in a comment rather than assuming. ### What to say in a review The short rule that covers almost every case: pass the mode you actually want the file to have, remember that the environment may only tighten it, and never assume the argument did anything at all if the file was already there.

  • How do you guarantee a newly created file ends up no more permissive than 0600?
    Pass `0600`. The mask can only clear bits, never set them, so the result is `0600` or tighter regardless of the environment. If you also need to know the file was not pre-existing with a looser mode, create it with `os.O_CREATE|os.O_EXCL` so an existing path is an error instead of a silent reuse.
  • Which bits of the fs.FileMode you hand to os.OpenFile does it actually use?
    The nine permission bits plus setuid, setgid and sticky. The type bits such as `fs.ModeDir` or `fs.ModeSymlink` are ignored — an open cannot create a directory or a symlink, so describing one in the mode has no effect.
  • Does the permission argument mean anything on Windows?
    Very little. Windows has no umask and no Unix mode bits; the `os` package largely ignores the value, with the owner write bit being the part that maps onto the read-only attribute. Confidentiality guarantees from a mode are a Unix-only assumption.

saying these in an interview costs you the question

  • Says the file will have exactly the mode that was passed
  • Thinks the umask is a Go setting or environment variable
  • Expects the permission argument to fix an existing file
  • Writes the mode in decimal instead of octal
  • Assumes a mode guarantee holds on Windows too