Skip to content

Harden configure-efa-fsx-lustre-client startup against IMDS race at boot - #1204

Merged
KeitaW merged 1 commit into
awslabs:mainfrom
davidk-mir:fix/efa-client-imds-race
Aug 13, 2026
Merged

Harden configure-efa-fsx-lustre-client startup against IMDS race at boot#1204
KeitaW merged 1 commit into
awslabs:mainfrom
davidk-mir:fix/efa-client-imds-race

Conversation

@davidk-mir

Copy link
Copy Markdown
Contributor

Purpose

configure_efa_lustre() in mount_fsx.sh (added in #849) downloads and runs the configure-efa-fsx-lustre-client sample from the FSx documentation, whose setup.sh installs a systemd service that queries EC2 instance metadata (IMDS) to configure the EFA/Lustre client.

That unit is Type=oneshot, ordered only on network-online.target, with no Restart= directive — and its IMDS queries have no retry. network-online.target means a link has an IP; it does not guarantee the IMDS endpoint is answering yet. On a cold boot the unit can start, miss IMDS by a few seconds, exit non-zero, and stay failed for the rest of the instance's life with nothing to retry it. The node then runs without the EFA LNet configuration and Lustre silently falls back to TCP — a failure mode easy to misdiagnose as a hardware/AMI problem.

Changes

  • Add harden_efa_client_service_startup(), invoked before setup.sh runs so the override is already in place when the unit is created and started:
    • installs /usr/local/sbin/wait-for-ec2-imds, a small readiness script that polls the IMDSv2 token endpoint plus a metadata read (short curl timeouts, 2-minute deadline);
    • installs a systemd drop-in for configure-efa-fsx-lustre-client.service with ExecStartPre= pointing at that script, Restart=on-failure / RestartSec=10s, and StartLimitIntervalSec=600 / StartLimitBurst=20 so retries aren't cut off by the default start-limit.
  • The vendor zip itself is not modified — this only changes how the installed unit is supervised.

Test Plan

Environment:

  • AWS Service: SageMaker HyperPod (Slurm), FSx for Lustre created with EFA enabled
  • Instance type: EFA-capable GPU instances
  • Number of nodes: multi-node cluster

Test commands:

bash -n 1.architectures/5.sagemaker-hyperpod/LifecycleScripts/base-config/mount_fsx.sh

# on a node after boot:
systemctl cat configure-efa-fsx-lustre-client.service   # shows the drop-in
journalctl -u configure-efa-fsx-lustre-client.service   # ExecStartPre polling, retries
systemctl is-active configure-efa-fsx-lustre-client.service

Test Results

  • Before: on cold boots where the unit's first IMDS query raced instance-metadata availability (observed unresponsive ~3s after network-online.target), the service failed within seconds of boot and remained permanently failed; kefalnd never loaded, no efa LNet net existed, and the Lustre mount ran over TCP with no visible error.
  • After: ExecStartPre waits for IMDS readiness before the vendor script runs; transient failures are retried per Restart=on-failure; the service reaches active and EFA transport verification (LNet send/recv counter deltas under generated I/O) passes.

Checklist

  • I have read the contributing guidelines.
  • I am working against the latest main branch.
  • I have searched existing open and recently merged PRs to confirm this is not a duplicate.
  • The contribution is self-contained with documentation and scripts.
  • External dependencies are pinned to a specific version or tag (no latest). (N/A — no new dependencies; the vendor zip URL is the one already used by this script)
  • A README is included or updated with prerequisites, instructions, and known issues. (N/A — focused hardening fix)
  • New test cases follow the expected directory structure. (N/A — not a test case)

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

… race at boot

configure_efa_lustre() downloads and runs the configure-efa-fsx-lustre-client
setup.sh sample, which installs a systemd service that queries EC2 instance
metadata (IMDS) to configure the EFA/Lustre client. That unit is Type=oneshot,
ordered only on network-online.target, with no Restart= directive.

network-online.target indicates the network interface is up, not that the
IMDS endpoint is actually answering requests. On a cold boot there is a
window where the unit can start, query IMDS, get nothing back, and exit
non-zero, and because there is no restart policy, the service is then
permanently in a failed state for the rest of the instance's life, with
nothing to prompt a retry once IMDS does come up moments later. Since this
happens before the EFA/Lustre client is actually configured, the failure
mode looks identical to EFA never being available on that instance, and is
easy to misdiagnose as a hardware/AMI issue instead of a boot-ordering race.

Add a systemd drop-in for configure-efa-fsx-lustre-client.service, installed
before setup.sh runs so it is already present when the unit is created and
started:
  - ExecStartPre runs a small readiness script that polls the IMDSv2 token
    endpoint and a metadata read (short curl timeouts) for up to ~2 minutes
    before allowing the main service to start.
  - Restart=on-failure / RestartSec=10s so a transient failure is retried
    instead of leaving the unit permanently failed.
  - StartLimitIntervalSec/StartLimitBurst sized generously so the retries
    are not throttled by systemd's default start-limit before IMDS comes up.

This only changes how the vendored systemd unit is supervised; it does not
modify the unit or scripts shipped in the vendor zip itself.
@svnisar

svnisar commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Author mentioned Test Results show EFA-transport check, which is a strong end-to-end signal. Please attach testing evidence if available.

@davidk-mir

Copy link
Copy Markdown
Contributor Author

@svnisar thanks for looking into this.

I recovered sanitized production lifecycle evidence from our custom Ansible-based implementation of the equivalent fix. We do not execute the upstream mount_fsx.sh directly; our lifecycle automation ports the relevant behavior into custom Ansible tasks.

It installs the same IMDS readiness helper and systemd override before executing AWS’s EFA/Lustre client setup, as done in this PR. After mounting FSx, it runs an equivalent end-to-end transport check, including the multi-interface counter aggregation proposed in #1203.

A representative run recorded in our logs today looks like:

  │ Install EC2 instance metadata readiness check
  │ Install systemd readiness and retry configuration
  │ Execute AWS EFA/Lustre client setup
  │ Confirm EFA client service is active
  │ Mount FSx
  │ Verify EFA transport

  │ PLAY RECAP
  │ localhost : unreachable=0 failed=0
  >

means: The transport check reads the aggregate EFA LNet send and receive counters, performs a 64 MiB direct write/read through the Lustre mount, and reads the counters again. It exits non-zero unless both totals increase. The verification task completes (ansible playbook ended with failed=0, providing us the end-to-end evidence that Lustre traffic used EFA.

Hence, our CloudWatch logs do not include the numeric counter output anymore, however, we did observe & verify non-zero counters manually when we first fixed this mount_fsx.sh script, and this logic landed in this PR.
I don't think it makes sense to contribute our entire ansible playbook lifecycle management extensions to this repo, that's why this is a stripped-down version of just the mount hardening.

Let me know if you have more questions

@KeitaW

KeitaW commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

@svnisar asked for testing evidence, so here it is. I verified this on real hardware too against a real EFA-enabled FSx for Lustre file system, including the failure path. Test environment and limitations are stated at the end.

Summary: the mechanism works. The drop-in does attach to the unit that setup.sh creates afterwards, ExecStartPre gates correctly in both directions, the unit is genuinely retried and does recover on its own, and EFA carries bulk data afterwards. I also checked the TimeoutStartSec interaction that would have made the script's 120 s deadline dead code, and it is not a problem. One gap remains that this PR does not close; details at the bottom as a suggested follow-up.

Before: the unpatched unit dies permanently

Vendor unit, no drop-in, IMDS unavailable (injected, see limitations):

systemctl start -> exit 1 after 137 s
Result=exit-code   ExecMainStatus=1   NRestarts=0
journal: RuntimeError: Failed to run "curl -s -X PUT
         "http://169.254.169.254/latest/api/token" ..."
         from get_instance_type(), configure-efa-fsx-lustre-client.py:531

I then restored IMDS and left the unit alone for 60 s: still failed, still NRestarts=0. It never recovers.

The reason this matters is the silent fallback. With the unit failed and no EFA LNet config, the mount still succeeded and I/O still worked, with no error surfaced:

172.31.41.130@tcp:/4pctzaev on /fsx type lustre (rw,noatime,...)
lnetctl net show -> lo and tcp only, no efa net
256 MiB write: 947 MB/s      256 MiB read: 838 MB/s      exit 0
lctl list_nids -> 172.31.36.134@tcp

Worth noting that verify_efa_transport() in this same script cannot catch that case, because it early-returns when EFA_CONFIGURED != true, which is exactly the state a failed unit leaves behind.

After, item 1: the drop-in really does attach

This was the sequencing claim I most wanted to check, since the drop-in is written before the unit exists.

# before setup.sh, drop-in already on disk:
systemctl cat configure-efa-fsx-lustre-client.service
  -> No files found for configure-efa-fsx-lustre-client.service   (exit 1)

# after setup.sh:
systemctl cat configure-efa-fsx-lustre-client.service
  -> # /etc/systemd/system/configure-efa-fsx-lustre-client.service
     ...vendor unit...
  -> # /etc/systemd/system/configure-efa-fsx-lustre-client.service.d/network-readiness.conf
     [Unit] After=cloud-init.service / Wants=cloud-init.service
     StartLimitIntervalSec=600 / StartLimitBurst=20
     [Service] ExecStartPre=/usr/local/sbin/wait-for-ec2-imds
     Restart=on-failure / RestartSec=10s

setup.sh neither removed nor rewrote it (file unchanged at 194 bytes). Effective properties: Type=oneshot, Restart=on-failure, RestartUSec=10s, StartLimitIntervalUSec=10min, StartLimitBurst=20, After=cloud-init.service basic.target network-online.target sysinit.target .... systemd-analyze verify returns 0 with no complaint about Restart= on a oneshot.

After, item 2: red path, retried and recovered

PR readiness script unmodified. IMDS blocked, then unblocked mid-loop:

01:19:27  IMDS blocked; systemctl start --no-block
01:21:27  [ERROR] EC2 instance metadata did not become ready within 2 minutes
          Control process exited, code=exited, status=1/FAILURE
01:21:37  Scheduled restart job, restart counter is at 1
01:23:38  second ExecStartPre failure
01:23:48  Scheduled restart job, restart counter is at 2
01:24:27  IMDS restored
01:24:44  Successfully configured and added 1 EFA interface(s)
01:24:48  active   NRestarts=2   Result=success

NRestarts climbing 0 → 1 → 2 with Scheduled restart job entries exactly 10 s apart is the Restart=on-failure / RestartSec=10s pair doing its job, and the unit reached active 21 s after IMDS came back with no intervention. Green path for completeness: with IMDS healthy ExecStartPre exits 0 in under 1 s. I also rebooted the instance with the drop-in in place and the unit came up active with NRestarts=0.

After, item 4: EFA transport in use

Same awk your verify_efa_transport() uses, 1 GiB O_DIRECT write plus 1 GiB read:

efa net: nid 36.134.47.0@efa  status up  interface rdmap47s0
lsmod: kefalnd 147456 1
send_count: 2 -> 3076 packets   (delta 3074 packets)
recv_count: 2 -> 4100 packets   (delta 4098 packets)
write 1.0 GiB in 1.03393 s (1.0 GB/s), read 1.0 GiB in 1.23022 s (873 MB/s)
tcp counters over the same window: send 36, recv 36 packets
udsp: src efa, priority 0

Thousands of packets on EFA against tens on TCP, so bulk data is on EFA and TCP carries only control traffic.

Non-regression on the non-EFA path

harden_efa_client_service_startup() sits behind both existing gates, so nodes and file systems without EFA never reach it:

case verdict harden called
EFA-enabled fs, same AZ EFA-compatible yes
non-EFA fs (cross-AZ) early return no
node with no EFA tooling early return no

bash -n passes, and shellcheck 0.9.0 reports 6 SC2155 findings on the PR file and the identical 6 on main, so no new findings. The extracted wait-for-ec2-imds is clean.

On TimeoutStartSec: checked, no change needed

I expected this to be a defect: ExecStartPre runs inside the start timeout, and if that were the usual 90 s then the script's 120 s deadline would be dead code. Measured on the node, it is the opposite:

DefaultTimeoutStartUSec=1min 30s        (manager level)
unit TimeoutStartUSec=infinity          (the real unit)
control on the same host: Type=simple -> 1min 30s ; Type=oneshot -> infinity

man 5 systemd.service under TimeoutStartSec=: "Defaults to DefaultTimeoutStartSec= set in the manager, except when Type=oneshot is used, in which case the timeout is disabled by default." Empirically, a oneshot with a 150 s ExecStartPre ran 144 s without being killed. The red-path log above also shows each attempt taking the full 120 s. So the deadline is live and no explicit TimeoutStartSec is needed.

Same man page settles the Restart= question, in case it comes up: "For Type=oneshot, Restart=always and Restart=on-success are not allowed" does not cover Restart=on-failure, and the node confirms it composes and works.

Two smaller checks: set -u with SECONDS is safe because SECONDS is always set in bash and ExecStartPre= execve's the file with its #!/usr/bin/env bash shebang, so dash never sees it; and the IMDS hop-limit concern does not apply because the script runs on the host rather than in a container. On this instance IMDSv2 was mandatory (IMDSv1 returned HTTP 401), so the token-PUT approach is required, not optional.

One suggested change: the lifecycle script's own IMDS call is still unprotected

verify_fsx_efa_compatibility() reads the instance AZ from IMDS with --max-time 3 and no retry, and it runs before the hardening is installed:

TOKEN=$(curl -X PUT ".../api/token" ... -s --max-time 3 2>/dev/null)
...
if [[ -z "$instance_az" ]]; then
    echo "[WARN] Could not determine instance AZ - proceeding without EFA verification"
    return 1
fi

If the lifecycle script itself hits the race this PR is about, that returns 1, configure_efa_lustre() returns early, and the hardening is never installed. I confirmed this with the same injection, including controls in both directions:

case result harden reached
EFA fs, IMDS up FSx filesystem is EFA-compatible yes
non-EFA fs, IMDS up different AZ ... not supported cross-AZ no
EFA fs, IMDS down [WARN] Could not determine instance AZ no
EFA fs, IMDS up again EFA-compatible yes

In the IMDS-down case configure_efa_lustre still returns rc=0, so the script reports success while skipping EFA for the node's whole lifetime, and since the unit is never created there is nothing to retry on the next boot. That lands the node in exactly the silent-TCP-fallback state shown at the top. Relevant too: the AWS CLI resolves its region from IMDS on these nodes (aws configure list shows region ... imds), so the describe-file-systems call in the same function fails by a second independent path.

Since you already install /usr/local/sbin/wait-for-ec2-imds, calling it (or an equivalent short retry loop) before the first IMDS read in verify_fsx_efa_compatibility() would close this. Happy to leave it as a follow-up issue if you would rather keep this PR narrow, but as it stands the fix is partial: it protects the unit's IMDS calls and not the lifecycle script's.

I also swept the repo for the same pattern elsewhere: 9 files query IMDS and 1 creates a Type=oneshot unit, and the intersection is empty, so no sibling script needs the same treatment.

Test environment and limitations

  • g6.8xlarge, DLAMI ami-0f29bf1fbe374da5f (Ubuntu 24.04.4, kernel 6.17.0-1019-aws), systemd 255, Lustre client 2.15.6, EFA driver 3.0.0g, kefalnd 1.2.2, 1 EFA interface.
  • FSx for Lustre PERSISTENT_2, EfaEnabled: true, 4800 GiB, 1000 MB/s/TiB, metadata AUTOMATIC, same AZ (us-east-1c) and same /16 as the client, EFA-enabled security group.
  • Vendor sample configure-efa-fsx-lustre-client.zip md5 ea03346719817100bcc2598d5b10761d.
  • Not tested on HyperPod. HyperPod was not available to me for this, so I ran the same lifecycle-script code paths on a standalone EC2 client against the same kind of file system. The systemd mechanism, the vendor setup.sh and the EFA transport are the same; HyperPod's own orchestration of lifecycle_script.py was not exercised. Ansible was installed manually since the DLAMI does not ship it.
  • The cold-boot race was simulated, not reproduced. Every failure case used iptables -I OUTPUT 1 -d 169.254.169.254 -j DROP, a total sustained IMDS outage, rather than the few seconds of early-boot unavailability a real race produces. That is harsher on the failure side, which suits testing a retry path, but it does not reproduce the natural timing. The one genuine boot I did test (instance reboot with the drop-in installed) came up clean on the first attempt, so it did not hit the race naturally.

@KeitaW
KeitaW merged commit 7962a99 into awslabs:main Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants