Skip to content

Protect supervisor memory from being read by sibling task processes#62523

Merged
ashb merged 1 commit intoapache:mainfrom
astronomer:supervisor-nondumpable
Feb 27, 2026
Merged

Protect supervisor memory from being read by sibling task processes#62523
ashb merged 1 commit intoapache:mainfrom
astronomer:supervisor-nondumpable

Conversation

@ashb
Copy link
Member

@ashb ashb commented Feb 26, 2026

Protect supervisor memory from being read by sibling task processes

Airflow task workers run all tasks as the same UID (unless you use run_as_user, which most people don't). Each supervisor process holds a distinct JWT token for API authentication. Without protection, any task process can read a sibling supervisor's memory and steal its token via:

  • /proc//mem (direct memory read)
  • /proc//environ (read environment variables)
  • /proc//maps (find memory layout, then read)
  • ptrace(PTRACE_ATTACH, ...) (debugger attach)

These all work because the kernel allows same-UID processes to access each other by default. And being able to have one task impersonate another task is not great for security controls we want to put in place.

Calling prctl(PR_SET_DUMPABLE, 0) tells the kernel to deny all four vectors for non-root processes without CAP_SYS_PTRACE. Root-level debugging tools (py-spy, strace, gdb under sudo) still work because CAP_SYS_PTRACE bypasses the dumpable check.

The flag is set at the top of supervise(), before the Client is constructed with the token. Since the task child is created via os.fork() with no subsequent execve(), it inherits the non-dumpable flag automatically — both supervisor and task processes are protected.

This is the same mechanism OpenSSH's ssh-agent uses to protect private keys in memory: openssh/openssh-portable@6c4914a and I think Chromium and KeePassXC etc use it similarly.

@ashb ashb force-pushed the supervisor-nondumpable branch 2 times, most recently from c87d729 to f2b5dcf Compare February 26, 2026 16:27
Copy link
Contributor

@amoghrajesh amoghrajesh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand a lot of the code, but from the tests, and reading up a bit what you say in your desc, and with the tests in place, I am fine

Airflow task workers run all tasks as the same UID (unless you use
run_as_user, which most people don't). Each supervisor process holds a
distinct JWT token for API authentication. Without protection, any task
process can read a sibling supervisor's memory and steal its token via:

  - /proc/<pid>/mem (direct memory read)
  - /proc/<pid>/environ (read environment variables)
  - /proc/<pid>/maps (find memory layout, then read)
  - ptrace(PTRACE_ATTACH, ...) (debugger attach)

These all work because the kernel allows same-UID processes to access
each other by default. And being able to have one task impersonate another
task is not great for security controls we want to put in place.

Calling `prctl(PR_SET_DUMPABLE, 0)` tells the kernel to deny all four
vectors for non-root processes without `CAP_SYS_PTRACE`. Root-level
debugging tools (py-spy, strace, gdb under sudo) still work because
`CAP_SYS_PTRACE` bypasses the dumpable check.

The flag is set at the top of supervise(), before the Client is
constructed with the token. Since the task child is created via
os.fork() with no subsequent execve(), it inherits the non-dumpable
flag automatically — both supervisor and task processes are protected.

This is the same mechanism OpenSSH's ssh-agent uses to protect private
keys in memory:
openssh/openssh-portable@6c4914a
and I think Chromium and KeePassXC etc use it similarly.
@ashb ashb force-pushed the supervisor-nondumpable branch from f2b5dcf to f6fd2bd Compare February 26, 2026 17:09
@ashb
Copy link
Member Author

ashb commented Feb 26, 2026

Updated to test it blocks /proc/pid/environ too (just for my sanity)

Copy link
Contributor

@jscheffl jscheffl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool!

@ashb ashb merged commit b656901 into apache:main Feb 27, 2026
191 of 192 checks passed
@ashb ashb deleted the supervisor-nondumpable branch February 27, 2026 10:43
AkshayArali pushed a commit to AkshayArali/airflow_630 that referenced this pull request Feb 28, 2026
…pache#62523)

Airflow task workers run all tasks as the same UID (unless you use
run_as_user, which most people don't). Each supervisor process holds a
distinct JWT token for API authentication. Without protection, any task
process can read a sibling supervisor's memory and steal its token via:

  - /proc/<pid>/mem (direct memory read)
  - /proc/<pid>/environ (read environment variables)
  - /proc/<pid>/maps (find memory layout, then read)
  - ptrace(PTRACE_ATTACH, ...) (debugger attach)

These all work because the kernel allows same-UID processes to access
each other by default. And being able to have one task impersonate another
task is not great for security controls we want to put in place.

Calling `prctl(PR_SET_DUMPABLE, 0)` tells the kernel to deny all four
vectors for non-root processes without `CAP_SYS_PTRACE`. Root-level
debugging tools (py-spy, strace, gdb under sudo) still work because
`CAP_SYS_PTRACE` bypasses the dumpable check.

The flag is set at the top of supervise(), before the Client is
constructed with the token. Since the task child is created via
os.fork() with no subsequent execve(), it inherits the non-dumpable
flag automatically — both supervisor and task processes are protected.

This is the same mechanism OpenSSH's ssh-agent uses to protect private
keys in memory:
openssh/openssh-portable@6c4914a
and I think Chromium and KeePassXC etc use it similarly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants