skip to content

On a PHP photo-contest site on a shared host, why does ini_set('upload_max_filesize', '20M') fail to raise the upload limit?

level: middleimportance: must knowfreq 52%

answer

  1. changeable modes: USER, PERDIR, SYSTEM, ALL
  2. upload_max_filesize is PERDIR
  3. ini_set returns false, no warning
  4. body already parsed before your code
  5. set it in .user.ini or .htaccess

basics

~20 s

upload_max_filesize is changeable only at INI_PERDIR or INI_SYSTEM level, so ini_set() from a script refuses it and returns false. The upload is also parsed before the script runs. Set it in php.ini, .user.ini or .htaccess instead.

solid answer

~40 s

Every directive has a **changeable mode**: `INI_USER` (scripts may change it), `INI_PERDIR` (php.ini, `.htaccess`, `httpd.conf` or `.user.ini`), `INI_SYSTEM` (php.ini or `httpd.conf`) and `INI_ALL` (anywhere). `upload_max_filesize` and `post_max_size` are `PERDIR`, so `ini_set()` refuses them: it returns `false` without a warning, and the value stays at its default `2M`. Even if it could change them, it would be too late, because PHP parses the request body before the first line of the script runs. On a shared host you set them per directory: in a `.user.ini` when PHP runs as FastCGI, including PHP-FPM, or with `php_value` in `.htaccess` when PHP runs as Apache's module, assuming the host allows it. `ini_get_all()` shows each directive's mode.

code

php · 8 lines
php
<?php
declare(strict_types=1);

var_dump(ini_set('upload_max_filesize', '20M')); // bool(false): INI_PERDIR
var_dump(ini_get('upload_max_filesize'));         // string(2) "2M"

var_dump(ini_set('memory_limit', '256M'));        // old value, e.g. string(4) "128M"
var_dump(ini_get('memory_limit'));                // string(4) "256M"

go deeper

for a junior

Remember that some settings, such as the upload size limit, cannot be changed from inside a script with ini_set().

for a middle

Name the four changeable modes, explain ini_set()'s silent false return, and why upload limits must be set before the script runs.

for a senior

Choose the right per-directory mechanism for the SAPI in use, check ini_set() results, and treat PERDIR and SYSTEM modes as the host's delegation boundary.

for a principal

Decide which limits tenants or teams may change themselves and which stay with operators, and make those settings visible in reviewed files.

## The symptom A photo-contest site lets entrants upload a 12 MB photo. Uploads fail, and the entrant's `$_FILES` entry reports that the file exceeded the size limit. A developer adds this to the top of the upload handler: ```php ini_set('upload_max_filesize', '20M'); ``` Nothing changes. `var_dump(ini_get('upload_max_filesize'))` still prints `2M`. ## Changeable modes Every ini directive is registered with a **changeable mode** that says where it may be set. The manual defines four, also exposed as PHP constants: | Constant | Where the directive may be set | |---|---| | `INI_USER` | in user scripts with `ini_set()`, the Windows registry, or `.user.ini` | | `INI_PERDIR` | php.ini, `.htaccess`, `httpd.conf` or `.user.ini` | | `INI_SYSTEM` | php.ini or `httpd.conf` only | | `INI_ALL` | anywhere, including `ini_set()` | `ini_set()` changes a directive only if its mode includes `INI_USER`, which `INI_ALL` does. Examples: - `memory_limit`, `max_execution_time`: `INI_ALL`, so `ini_set()` works; - `upload_max_filesize`, `post_max_size`, `max_file_uploads`, `output_buffering`: `INI_PERDIR`, so it does not; - `disable_functions`, `extension_dir`, `file_uploads`: `INI_SYSTEM`, so only server-level configuration can set them. ## What ini_set() returns `ini_set(string $option, string|int|float|bool|null $value): string|false` returns the **old value** as a string on success and `false` on failure. Refusing a `PERDIR` directive is a failure, but it emits **no warning**, so the call looks harmless unless you check the return value: ```php if (ini_set('upload_max_filesize', '20M') === false) { // not changeable from a script } ``` ## Too late anyway Even for a directive that `ini_set()` could change, timing matters. For a `multipart/form-data` POST, PHP reads and parses the request body, enforcing the upload and body limits, **before** your script's first line runs. By the time `ini_set()` executes, the oversized file has already been rejected. That is exactly why these directives are not script-changeable. ## Where to set them on a shared host Without access to php.ini, use the per-directory mechanism that matches how PHP runs: 1. **PHP as FastCGI** (PHP-FPM or `php-cgi`): put the values in a **`.user.ini`** file in the document root or the upload handler's directory. 2. **PHP as Apache's module** (`apache2handler`): use `php_value upload_max_filesize 20M` in `.htaccess`, if the host's Apache configuration allows overrides. 3. **Anywhere you control the server**: php.ini, or a file in the scan directory. Raise both limits together: the file limit and the whole-body limit, which must be at least as large. How the two interact is part of receiving uploads, a separate topic. ## A decision table | Directive mode | `ini_set()` | `.user.ini` | `.htaccess` (`php_value`) | php.ini | |---|---|---|---|---| | `INI_ALL` | yes | yes | yes | yes | | `INI_PERDIR` | no | yes | yes | yes | | `INI_SYSTEM` | no | no | no | yes | The `.user.ini` column applies only when PHP runs as CGI or FastCGI, and the `.htaccess` column only under Apache's module. Reading the table for the photo-contest site: the limits are `PERDIR`, the host runs PHP-FPM, so `.user.ini` is the tool. ## Common mistakes - Assuming `ini_set()` worked because no warning appeared. - Raising the per-file limit but not the whole-body limit, so large uploads still fail. - Testing the new value from the shell, where neither `.user.ini` nor `.htaccess` applies. - Using `php_value` in `.htaccess` on a FastCGI setup, where no PHP module inside Apache exists to apply it. ## How to check a directive's mode `ini_get_all('core')` returns, for each directive, its `global_value`, `local_value` and `access`, where `access` is the bitmask of the modes above. The manual's list of php.ini directives also gives each one's mode. ## Why the modes exist Modes let a host delegate safely. Letting any script raise upload or body limits would let one tenant's code accept huge requests that tie up the server; letting it change `disable_functions` would defeat hardening. `PERDIR` puts such knobs in files an operator can see and audit, and `SYSTEM` keeps the dangerous ones with the server's owner.

  • Why doesn't ini_set() warn when it refuses a directive?
    It reports failure only through its return value: false instead of the old value. Code that ignores the return value never learns the change was refused, so check it with === false, or confirm with ini_get().
  • Which directives can a script change with ini_set()?
    Those whose changeable mode includes INI_USER, which INI_ALL does. memory_limit and max_execution_time qualify; PERDIR ones such as upload_max_filesize and SYSTEM ones such as disable_functions do not. ini_get_all() reports each directive's access bitmask.
  • The host runs PHP as an Apache module. Will a .user.ini work?
    No. .user.ini files are processed only by the CGI and FastCGI SAPIs, including PHP-FPM. Under Apache's module, per-directory settings go in .htaccess with php_value or php_flag, which the Apache configuration must allow.

saying these in an interview costs you the question

  • ini_set() can change any directive for the current request.
  • ini_set() throws an exception when a directive is not changeable.
  • Calling ini_set() before move_uploaded_file() raises the upload limit in time.
  • upload_max_filesize is an INI_SYSTEM directive only php.ini can set.
  • .user.ini works the same under Apache's module as under PHP-FPM.