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.
| 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:
-
.envfiles 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 — readgetfaclat that point. - It does not model your filesystem's quirks. A mount with
noexecdefeats 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.