70.1.44. pepsi-helper-maildir-writer

drop a message into a local user’s Maildir/new/ as root

Manual section:

1

70.1.44.1.1. Name

pepsi-helper-maildir-writer - privileged local-delivery helper that writes a single message from standard input into a target user’s Maildir/new/.

70.1.44.1.2. Synopsis

pepsi-helper-maildir-writer [OPTIONS] TARGET-UID < message

pepsi-helper-maildir-writer measure [OPTIONS] TARGET-UID

70.1.44.1.3. Description

pepsi-helper-maildir-writer is a minimal, security-hardened helper that delivers exactly one message into a local user’s Maildir. It exists so that a trusted but unprivileged caller (a member of the pepsi-maildir group) can drop mail into any local user’s mailbox — and do nothing else.

The helper is installed setuid-root, owned root:pepsi-maildir with mode 4750 (rwxr-x--- plus the setuid bit). Consequently only members of the pepsi-maildir group may execute it; when they do, the kernel runs it with an effective uid of root.

Given the numeric uid of the target user, and the raw message on standard input, the helper:

  1. resolves the target user’s passwd entry (its home directory and primary group), refusing uid 0 and any uid below /etc/login.defs UID_MIN (default 1000 if unreadable) — system accounts are never valid delivery targets;

  2. fully and irreversibly drops privilege from root to the target user — real, effective and saved uid and gid — first dropping all supplementary groups, then the gid, then the uid, and asserts that root cannot be regained. Everything afterwards acts with the target user’s identity, so any file it creates is owned by that user;

  3. creates the standard Maildir layout (tmp, new and cur, mode 0700) under ~/Maildir if it does not already exist;

  4. creates a uniquely named file in Maildir/tmp/ with O_CREAT|O_EXCL (so it has exclusive control over the file and never disturbs another delivery’s file), mode 0600, following the Maildir naming convention <seconds>.M<microseconds>P<pid>Q<sequence>.<hostname>;

  5. copies the message from standard input into that file, using splice(2) when the descriptors allow it (the common case, when stdin is a pipe) and a plain read/write loop otherwise, counting the bytes as they pass;

  6. fsync(2)s the file, and — when a quota is being enforced and --measure was given — decides whether the mailbox has room for it, abandoning the delivery with exit status 3 if it has not; and

  7. renames it into Maildir/new/ under a name carrying the Maildir++ ,S=size tag, fsync(2)s that directory, and reports what the mailbox now holds on standard output.

The helper reads no configuration, opens no network connection and contacts no database. Its narrow scope is the whole point of the privilege separation: the quota policy is decided by the caller and passed in as arguments, because by the time the helper could read anything it is the target user, who does not get to choose their own quota.

70.1.44.1.4. Mailbox quota

The helper is the only part of Pepsi that can see how full a mailbox is: a Maildir is mode 0700, and this is the one process that becomes the user. Everything else — the delivery stage’s decisions, pepsi-ingress(1)’s refusal at RCPT, the pepsi-status(1) report — works from figures this helper reported.

It measures only when asked (--measure). The caller keeps a running estimate in pepsi.mailbox_quota and asks for a measurement when that estimate suggests the account may be near its limit; because the estimate only ever over-counts (nothing tells it when a user deletes mail over IMAP), “no measurement needed” is a proof that there is room. An ordinary delivery to a mailbox that is nowhere near full therefore costs one small append and no directory walk.

Three accounting layers are used together, cheapest first:

maildirsize

The Maildir++ size file at the root of the mailbox, in Courier’s format (a quota definition line, then appended <bytes> <messages> deltas). Dovecot’s quota = maildir: backend, Courier and Exim’s maildir_use_size_file all read and write the same file, so a site running one of them for IMAP and Pepsi for delivery keeps one set of numbers. It is used when it is current, and rewritten from a full walk when it is missing, unparseable, computed against different limits, or has accumulated a long delta log. It lives in the user’s own directory, so it is advisory: a user can rewrite their own maildirsize.

A full tree walk

new/ and cur/ of the INBOX and of every Maildir++ folder (.Sent, .Trash, …) — the whole tree, which is what the user calls “my mailbox”. tmp/ is not counted (a file there is a delivery in flight). The walk is one readdir(3) per folder and no stat(2) calls, because Maildir++ encodes each message’s size in its file name (,S=n) — which is exactly why this helper tags the files it writes. A message left untagged by some other agent is stated individually.

quotactl(2)

With --fs-quota, the kernel’s own accounting is consulted as well. This is the only tamper-proof layer, and its limit tightens the effective one: promising space that EDQUOT would refuse helps nobody. It works unprivileged because Q_GETQUOTA is permitted for a caller asking about its own uid, and by this point the helper has irreversibly become that user. Note the kernel counts every block the user owns on that filesystem, not just their mail, so its figures are reported separately rather than replacing the mailbox measurement. A filesystem that does not answer (XFS project quotas, ZFS, no quota support) simply yields nothing, and the other layers still apply.

Because the message’s size is unknown until standard input has been drained, the message is buffered in tmp/ (which does not count towards the quota) before the decision is made, and unlinked if it does not fit. Standard input is drained in every case: a helper that exited early would leave its caller writing into a broken pipe, which would be reported as a transport failure rather than the clean refusal it is.

Two deliveries running at once may both see room and both write, overshooting the limit by up to one message each. Every implementation of Maildir++ quota accepts that; the kernel-quota layer, where a site has one, closes it exactly.

70.1.44.1.5. Arguments

TARGET-UID

The numeric user id of the local user into whose Maildir/new/ the message is delivered. It must be a positive integer with a matching passwd entry; uid 0 (root) and any uid below /etc/login.defs UID_MIN are refused.

measure

Measure the mailbox and report, delivering nothing. Standard input is not read. Used by pepsi-quota(1)’s measure and reconcile.

--measure

Establish the mailbox’s true size before writing, and enforce the quota given by --quota-bytes/--quota-count against it. Without this flag the helper delivers unconditionally and reports only what it added.

--fs-quota

Also consult quotactl(2), report the kernel’s figures, and let the kernel’s limit tighten the effective one. Set by the delivery stage from [pepsi] MAILBOX_FS_QUOTA, which pepsi-setup(1) probes for.

--quota-bytes N

The byte limit to enforce. 0 or absent means no limit.

--quota-count N

The message limit to enforce. 0 or absent means no limit.

The message to deliver is read in its entirety from standard input.

70.1.44.1.6. Output

On standard output the helper writes one tagged, tab-separated record per line. An unknown tag is ignored by the caller rather than being an error, so a newer helper can report more to an older caller.

uTABbytesTABmessages

Measured usage of the whole Maildir++ tree, including the message just delivered. The caller stores this as the account’s usage and resets its since-measurement counters.

fTABusedTABlimitTABinodesTABinode-limit

The quotactl(2) figures, with 0 where the kernel enforces no limit.

dTABbytes

Bytes this run added, reported instead of u when no measurement was made, so the caller adds to its estimate rather than replacing it.

sTAB(fsquota|maildirsize|scan)

How the u figure was obtained.

wTABtext

A non-fatal problem (an unreadable folder, a maildirsize that could not be updated). Never fatal to the delivery.

A report is written even when the delivery was refused for want of space — especially then, since a fresh measurement is what licenses pepsi-ingress(1) to refuse the next message in the SMTP session rather than accepting it only to bounce it.

70.1.44.1.7. Installation

The binary must be installed setuid-root and group-restricted:

chown root:pepsi-maildir pepsi-helper-maildir-writer
chmod 4750 pepsi-helper-maildir-writer

make install does this automatically (its install-helper step), provided it is run as root and the pepsi-maildir group exists; otherwise it prints the exact commands to run by hand. If the helper is placed on a filesystem mounted nosuid, the setuid bit is ignored at runtime and the helper will refuse to run (it cannot gain root), so install it on a normal filesystem.

70.1.44.1.8. Exit Status

The exit code tells the calling stage whether retrying can ever help, so a message is neither bounced while it could still be delivered nor queued for days against a mailbox that will never accept it. A diagnostic is always written to standard error, and pepsi-stage-relay-to-maildir(1) both logs it and puts it into the bounce it generates.

0

The message was written to the target user’s Maildir/new/ and synced to disk.

1

Transient failure: a later attempt may succeed, so the caller keeps the message queued and retries with backoff. This covers a full disk (ENOSPC), an I/O error, a read-only filesystem, and a helper that is not (yet) installed setuid-root — all of which an administrator can repair underneath a queued message.

2

Permanent failure: the mailbox is unusable and no retry will change that until an administrator fixes the account, so the caller bounces the message at once. This covers a Maildir that is not a directory (a legacy mbox file), ownership or a mode that locks the user out of their own Maildir (EACCES/EPERM), a missing home directory (ENOENT), a uid with no passwd entry, a home directory that is not absolute, and a uid below UID_MIN.

3

The mailbox is over quota, so the message was not written. Deliberately neither transient nor permanent: whether a full mailbox is worth waiting for is a site policy ([pepsi] MAILBOX_OVER_QUOTA), and the helper is in no position to know whether this user empties their mailbox daily or abandoned it a year ago. It reports the fact; pepsi-stage-relay-to-maildir(1) applies the policy. EDQUOT from the kernel is reported the same way — a full disk is the administrator’s problem and gets fixed underneath a queued message, whereas a full mailbox is the recipient’s and may never be.

When invoked by a user who is not a member of the pepsi-maildir group, the kernel refuses to execute the binary at all (Permission denied); the helper itself never runs.

70.1.44.1.9. Security

The helper trusts its caller to be authorised (membership of the pepsi-maildir group is the access-control gate) and will deliver to any regular local user (uid UID_MIN, never a system account). It drops supplementary groups before changing uid and then sheds root completely (real, effective and saved ids), asserting root cannot be regained before it does any file I/O, and creates the delivery file exclusively so it can never be tricked into writing over a file it does not own.

70.1.44.1.10. See Also

pepsi-stage-relay-to-maildir(1), pepsi-ingress(1), pepsi-dispatch(1), pepsi.conf(5), maildir(5)

70.1.44.1.11. Bugs

Report bugs to the Pepsi issue tracker.