ChmodCalc

About this site

ChmodCalc is a permission calculator with an unusually specific claim attached: that its tables are either generated or quoted, and never written from memory. This page is where that claim is made checkable, including the parts where it fails.

What is generated

Three datasets are produced by iterating the mode space at build time, with no cell typed by hand:

  • The 512-mode table on the front page — every three-digit value from 000 to 777, with its symbolic form and per-class meaning. The calculator, the table and the test suite all call the same functions.
  • The 512-value umask table, produced by applying the POSIX rule to every value, not just to the handful that get quoted everywhere.
  • Every "N of M" sentence on the site. Each one calls a counting function over the mode space. None of them is a literal number in the prose, which is the only way to keep a counting claim honest when the data underneath it changes.

The full mode space including the setuid, setgid and sticky bits is 4096 values, and it is worth being exact about what the test suite proves over them. It does not compare the symbolic renderer against a second copy of itself; a second copy agreeing would prove very little. Instead it enumerates the symbolic alphabet from the notation's own definition — two states for each read and write position, four for each execute position, because each of those carries a special bit as well — which yields 4096 distinct strings, and then proves the renderer is a bijection between those strings and the 4096 modes: every string parses, no two collide, every mode is hit exactly once, and every mode survives the round trip. Hand-checked specimens covering each of the s, S, t and T cases pin the orientation, so a renderer that was consistently inverted would still fail. The umask columns and the mode table's word columns are the parts genuinely recomputed a second way — from the digits, with the functions that produced them left out of the check.

What is quoted, and from where

The error index quotes real strings from the programs that print them. Every source below was opened and read on 2026-08-15; none of them is cited on the strength of a summary or a memory. The build fails if a quoted string in the data module drifts from what the pages render.

The 26 sources behind every factual claim on this site.
Project or standard What was read Symbol or section How
OpenSSH (portable) authfile.c sshkey_perm_ok() C source read directly from the project’s master branch
OpenSSH (portable) misc.c safe_path() C source read directly from the project’s master branch
OpenSSH (portable) auth2-pubkeyfile.c auth_openfile() C source read directly from the project’s master branch
OpenSSH (portable) readconf.c read_config_file_depth(), SSHCONF_CHECKPERM branch C source read directly from the project’s master branch
OpenSSH (portable) ssh.1 — FILES ssh(1) manual page source mdoc manual source read directly from the project’s master branch
OpenSSH (portable) sshd_config.5 — StrictModes sshd_config(5) manual page source mdoc manual source read directly from the project’s master branch
Apache HTTP Server server/request.c ap_directory_walk(), APLOGNO(00035) C source read directly from the project’s trunk
Apache HTTP Server modules/aaa/mod_authz_core.c authorize_user_core(), APLOGNO(01630) C source read directly from the project’s trunk
nginx src/http/modules/ngx_http_static_module.c ngx_http_static_handler() C source read directly from the project’s master branch
nginx src/core/ngx_log.c ngx_log_errno() C source read directly from the project’s master branch
cronie src/database.c check_open() C source read directly from the project’s master branch
cronie src/misc.c log_it() C source read directly from the project’s master branch
sudo plugins/sudoers/sudoers.c open_sudoers() C source read directly from the project’s main branch
Git hook.c find_hook() C source read directly from the project’s master branch
Git advice.c vadvise() C source read directly from the project’s master branch
sudo lib/util/secure_path.c sudo_check_secure() C source read directly from the project’s main branch
systemd src/shared/conf-parser.c config_parse() C source read directly from the project’s main branch
systemd src/basic/fs-util.c stat_warn_permissions() C source read directly from the project’s main branch
systemd src/core/load-fragment.c unit_load_fragment() C source read directly from the project’s main branch
Linux kernel fs/inode.c inode_init_owner() C source read directly from the kernel’s master branch
Linux kernel fs/exec.c bprm_fill_uid() C source read directly from the kernel’s master branch
POSIX.1-2024 (IEEE Std 1003.1-2024) XCU §2.8.2 Exit Status for Commands Shell Command Language The Open Group’s own free HTML edition, read directly
POSIX.1-2024 (IEEE Std 1003.1-2024) XSH umask() — DESCRIPTION umask() The Open Group’s own free HTML edition, read directly
POSIX.1-2024 (IEEE Std 1003.1-2024) XCU chmod — EXTENDED DESCRIPTION chmod utility The Open Group’s own free HTML edition, read directly
Linux kernel include/uapi/asm-generic/errno-base.h EACCES / EPERM header read directly from the kernel’s master branch
POSIX.1-2024 (IEEE Std 1003.1-2024) XBD §4.5 Directory Protection S_ISVTX The Open Group’s own free HTML edition, read directly

A note on what "read directly" means here. For the C projects it means the source file on the project's own repository, at the branch named in the table — not a distribution patch set, and not a mailing-list quotation. For POSIX it means The Open Group's free HTML edition of IEEE Std 1003.1-2024, which is published without a paywall. Both kinds of source can be opened by you in one click from the table above, which is the property that makes the claim worth anything.

What could not be verified

1 of the 14 rows in the deployment-context table is marked convention, and the label is load-bearing, not decorative:

  • .env files at 600. No loader whose behaviour was checked here refuses to read a world-readable .env. Unlike an SSH private key, nothing stops you. The reason to use 600 is that everything else on a shared host can read a 644 file — an argument from consequence, not from enforcement, and labelled as such.

A correction that used to live in that list

A second row sat under the heading above and no longer does, which is worth recording out in the open. systemd unit files at 644 were labelled convention on the strength of a search that missed the check — the page said no permission check on unit files existed. One does. unit_load_fragment() calls config_parse(), and config_parse() calls stat_warn_permissions() on the file it is about to read. That function warns when st_mode & 0111 is set ("marked executable"), when st_mode & 0002 is set ("marked world-writable"), and — only when getpid_cached() == 1, which is exactly when PID 1 loads a system unit — when (st_mode & 0044) != 0044 ("marked world-inaccessible"). Those three guards together describe the shape of 644 and nothing else.

Each warning ends with the words "Proceeding anyway", so this is not enforcement: a unit at 600 or at 755 still loads and still starts. That is why the row is now marked checked and was not moved to enforced, and why a fifth basis label had to exist at all. The distinction being drawn is between a program that refuses, a program that states a preference in its log and continues, and a habit with nothing behind it — and collapsing the middle case into either neighbour would have been a second error dressed up as a fix.

Call chain read in full: src/core/load-fragment.c — unit_load_fragment(), then src/shared/conf-parser.c — config_parse(), then src/basic/fs-util.c — stat_warn_permissions(). C source read directly from the project’s main branch.

A third gap is worth stating because it affects how you should read two columns. The nginx and Apache rows in the error index assume the worker process is neither the file's owner nor a member of its group, so the "others" digit is what decides. That is the usual arrangement and it is not universal; if you have added the worker to the owning group, read the group digit instead. The assumption is printed in the row itself, not buried down here.

Finally: the error index contains 12 rows, of which 2 fire on no mode at all. Those are included deliberately. They are the messages that look like permission problems, get "fixed" with chmod 777, and are not permission problems — an index that only listed chmod-fixable errors would quietly encourage the exact mistake it should prevent.

What this site does not do

  • It does not run anything on your machine. The calculator is JavaScript in your browser. There is no server component, no account and no request while you type; the path box exists so the generated command is paste-ready, and that path never leaves the tab.
  • It does not model ACLs, SELinux, AppArmor, or capabilities. All four can grant or deny access independently of the twelve mode bits. A listing whose permission field ends in + has an ACL attached and the mode is no longer the whole story — read getfacl at that point.
  • It does not model your filesystem's quirks. A mount with noexec defeats every execute bit on it; a FAT or NTFS volume mounted on Linux synthesises modes from mount options and stores none of its own; and a network filesystem may map identities in ways that make the local mode misleading.
  • It does not know your distribution's defaults. Where a claim would depend on which distribution or version you are running, this site says what the arithmetic does and stops there.

Corrections

If a quoted string, a mask or a count is wrong, that is a defect and not a matter of opinion — every one of them is checkable against a linked source or reproducible by iterating the mode space yourself. The contact page explains how to report one and what to include.

elsewhere on this site

other tools we run