Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/docker-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,42 @@ describe('docker-manager', () => {
}
});

it('should skip env vars exceeding MAX_ENV_VALUE_SIZE from env-all passthrough', () => {
const largeVarName = 'AWF_TEST_OVERSIZED_VAR';
const saved = process.env[largeVarName];
// Create a value larger than 64KB
process.env[largeVarName] = 'x'.repeat(65 * 1024);

try {
const configWithEnvAll = { ...mockConfig, envAll: true };
const result = generateDockerCompose(configWithEnvAll, mockNetworkConfig);
const env = result.services.agent.environment as Record<string, string>;

// Oversized var should be skipped
expect(env[largeVarName]).toBeUndefined();
} finally {
if (saved !== undefined) process.env[largeVarName] = saved;
else delete process.env[largeVarName];
}
});

it('should pass env vars under MAX_ENV_VALUE_SIZE from env-all passthrough', () => {
const normalVarName = 'AWF_TEST_NORMAL_VAR';
const saved = process.env[normalVarName];
process.env[normalVarName] = 'normal_value';

try {
const configWithEnvAll = { ...mockConfig, envAll: true };
const result = generateDockerCompose(configWithEnvAll, mockNetworkConfig);
const env = result.services.agent.environment as Record<string, string>;

expect(env[normalVarName]).toBe('normal_value');
} finally {
if (saved !== undefined) process.env[normalVarName] = saved;
else delete process.env[normalVarName];
}
});

it('should auto-inject GH_HOST from GITHUB_SERVER_URL when envAll is true', () => {
const prevServerUrl = process.env.GITHUB_SERVER_URL;
const prevGhHost = process.env.GH_HOST;
Expand Down
44 changes: 44 additions & 0 deletions src/docker-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ const API_PROXY_CONTAINER_NAME = 'awf-api-proxy';
const DOH_PROXY_CONTAINER_NAME = 'awf-doh-proxy';
const CLI_PROXY_CONTAINER_NAME = 'awf-cli-proxy';

/**
* Maximum size (bytes) of a single environment variable value allowed through
* --env-all passthrough. Variables exceeding this are skipped with a warning
* to prevent E2BIG errors from ARG_MAX exhaustion.
*/
const MAX_ENV_VALUE_SIZE = 64 * 1024; // 64 KB

/**
* Total environment size (bytes) threshold for issuing an ARG_MAX warning.
* Linux ARG_MAX is ~2 MB for argv + envp combined; warn well before that.
*/
const ENV_SIZE_WARNING_THRESHOLD = 1_500_000; // ~1.5 MB

/**
* Flag set by fastKillAgentContainer() to signal runAgentCommand() that
* the container was externally stopped. When true, runAgentCommand() skips
Expand Down Expand Up @@ -794,11 +807,27 @@ export function generateDockerCompose(

// If --env-all is specified, pass through all host environment variables (except excluded ones)
if (config.envAll) {
const skippedLargeVars: string[] = [];
for (const [key, value] of Object.entries(process.env)) {
if (value !== undefined && !EXCLUDED_ENV_VARS.has(key) && !Object.prototype.hasOwnProperty.call(environment, key)) {
// Skip oversized values to prevent E2BIG (Argument list too long) errors.
// The Linux kernel enforces ARG_MAX (~2MB) on argv+envp combined; large env
// vars can exhaust this budget, especially when combined with large prompts.
const valueSizeBytes = Buffer.byteLength(value, 'utf8');
if (valueSizeBytes > MAX_ENV_VALUE_SIZE) {
skippedLargeVars.push(`${key} (${(valueSizeBytes / 1024).toFixed(0)} KB)`);
continue;
}
environment[key] = value;
}
}
if (skippedLargeVars.length > 0) {
logger.warn(`Skipped ${skippedLargeVars.length} oversized env var(s) from --env-all passthrough (>${(MAX_ENV_VALUE_SIZE / 1024).toFixed(0)} KB each):`);
for (const entry of skippedLargeVars) {
logger.warn(` - ${entry}`);
}
logger.warn('Use --env VAR="$VAR" to explicitly pass large values if needed.');
}
} else {
// Default behavior: selectively pass through specific variables
if (process.env.GITHUB_TOKEN) environment.GITHUB_TOKEN = process.env.GITHUB_TOKEN;
Expand Down Expand Up @@ -903,6 +932,21 @@ export function generateDockerCompose(
}
}

// Warn when total environment size approaches ARG_MAX (~2MB).
// Linux enforces a combined argv+envp limit; large environments can cause E2BIG errors
// when execve() is called inside the container.
if (config.envAll) {
const totalEnvBytes = Object.entries(environment)
.reduce((sum, [k, v]) => sum + k.length + (v?.length ?? 0) + 2, 0); // +2 for '=' and null
if (totalEnvBytes > ENV_SIZE_WARNING_THRESHOLD) {
logger.warn(
`⚠️ Total container environment size is ${(totalEnvBytes / 1024).toFixed(0)} KB — ` +
'may cause E2BIG (Argument list too long) errors when combined with large command arguments'
);
logger.warn(' Consider using --exclude-env to remove unnecessary variables');
}
}

// DNS servers for Docker embedded DNS forwarding (used in docker-compose dns: field)
const dnsServers = config.dnsServers || DEFAULT_DNS_SERVERS;
// Pass DNS servers to container so setup-iptables.sh can allow Docker DNS forwarding
Expand Down
Loading