-
Notifications
You must be signed in to change notification settings - Fork 17
Issue 107 Signed Out While Still Working
Editing a task and saving it dropped you back at the login screen. Tickets and assets were fine; only tasks did it. It could not be reproduced by two other people running the same version.
Reported in issue #107 by tjedelhauser, who narrowed it to tasks and established that an older instance of theirs did not do it. mbsouth did the equally valuable work of showing it did not happen on a native Debian install, which is what made it clear the difference was environmental rather than in the tasks code.
Fixed in fd5d98b4, released as update #1260.
FreeITSM was letting PHP delete the sessions of people who were actively using it. Nothing to do with tasks, and nothing to do with Docker, though it presents as both.
Your session lives in a small file on the server. PHP tidies away sessions that have been abandoned, and to decide whether a session is abandoned it asks one question:
When was this file last written to?
Not when was it last read. Not is that person still clicking things. Just: when did it last change.
FreeITSM reads your session on every single request β that is how it knows who you are β but it only changes it when something about you changes, which is almost never. So while you sat there working, your session file was being read hundreds of times and written zero times.
After 24 minutes of that, PHP looked at the file, saw nothing had changed since you signed in, concluded you had gone home, and deleted it. You were mid-sentence.
Constant use looked exactly like total inactivity.
The one thing that saved you was loading a full page, which does update the file. That is the entire reason this looked like a bug in Tasks.
It does not announce itself. It goes like this:
- You are working on the Tasks board, editing a task in the panel. Everything is normal.
- At some point the page quietly stops responding. You click a task and it does not open. You change a field and nothing happens. No error, no message, nothing in the way.
- You assume the page has got itself stuck, so you refresh.
- You are on the login screen.
Step 2 is the part that makes it confusing. The session is already gone by then, so every background request is being refused β but the Tasks screen does not say so, it simply does nothing. The logout only becomes visible when you reload, which makes it feel as though refreshing caused it.
Because the Tasks board is a single page.
You open it once and then work inside it: the detail panel, the fields, the subtasks are all updated in the background without the page ever reloading. You can easily spend half an hour there. During all of that, nothing writes to your session file, so the clock never restarts.
Ticket work is different. You move between the inbox, a ticket, a search, settings β and every one of those page loads updates the session file, resetting the 24 minutes. You would have to work unusually still for a ticket session to age out.
So the tasks code was never at fault. Tasks was simply the one place you could sit still for long enough.
This is the interesting part, and worth writing down.
Being logged out of a web application is not, on the face of it, a bug. It is a thing that happens. You shrug, sign back in, and carry on with your day.
Almost nobody reports "I got logged out". It looks like the software working as intended β arguably like the software being careful. The people this was happening to were very likely writing it off as a security timeout, which is exactly what it looked like. It had been in the product since 25 May.
Even somebody who suspected a fault would struggle to report it. It needs 24 minutes of a particular kind of work, and then a one-in-a-hundred roll of the dice. Sit down to reproduce it deliberately and you will fail, because reproducing things involves clicking around β which reloads pages, which resets the clock. Trying to catch it is what prevents it.
The reporter succeeded only because they hit it repeatedly during real work, with the one workflow that keeps you on a single page.
PHP's tidying is a dice roll on each request, and how loaded the dice are depends entirely on how PHP was installed. This is measured, not assumed:
| Install | Setting | How often the collector runs |
|---|---|---|
Official php:*-apache Docker image |
gc_probability 1, gc_divisor 100
|
1 request in 100 β fires during an ordinary afternoon |
Typical WAMP (php.ini-development) |
gc_probability 1, gc_divisor 1000
|
1 in 1000 β ten times rarer |
| Debian / Ubuntu packages | gc_probability 0 |
Never in-process; a cron job sweeps every 30 minutes |
So the reporter, on Docker, met it regularly. On a development machine you would be unlucky to see it, and a developer reloading pages constantly resets the clock long before it matters.
Debian is not immune either, which is worth stating clearly because it is tempting to conclude otherwise. Its sessionclean cron deletes with find -cmin, and a file's change time is no more refreshed by reading it than its modification time is. The blind spot is identical β it simply gets one chance every half hour instead of one per hundred requests. That reads as immunity in a short test without being immunity.
The trigger needs both halves: 24 minutes with no page load, and the collector happening to run. Change either and the bug disappears. That is why two people testing the same version in good faith could not reproduce it, and it is a much better explanation than "their platform is unaffected".
PHP's file session handler deletes session files older than session.gc_maxlifetime β 1440 seconds, 24 minutes β judged by the file's timestamp.
That is normally kept honest for you. session.lazy_write is on by default, and it does something subtle and helpful: when a session closes and the data has not changed, PHP skips the pointless rewrite but still touches the file. That single touch is what stops an active session ever looking idle.
FreeITSM's data endpoints never reach that close:
session_start(['read_and_close' => true]);They read the session and shut it immediately. That is deliberate and correct β it releases the session lock so one slow request cannot block every other request from the same browser, which is why it was applied across the codebase in #389 (25 May 2026). But closing that way skips the timestamp update entirely.
717 endpoints open the session like this. Every one of them kept you signed in in every sense except the one being measured.
That date is also the answer to the reporter's own observation that an older instance of theirs was unaffected: it predates #389.
includes/session_keepalive.php restores the timestamp update that read_and_close skips, and does nothing else. One stat and one touch: no session lock, no re-open, so the reason read_and_close exists is untouched.
It is included from functions.php alongside the other session concerns that run on include, because all 717 endpoints already include that file β one require rather than 717.
Sessions that are genuinely idle still expire on exactly the old schedule. The refresh only happens while a request is actually being served, which is the correct definition of "still in use" and the one PHP intended all along. This is a fix for miscounting, not a way of keeping sessions alive longer.
| File | What changed |
|---|---|
includes/session_keepalive.php |
New. Refreshes the session file's timestamp when the session was opened with read_and_close. |
includes/functions.php |
Includes it, next to session_security.php, request_guard.php and password_gate.php. |
session_start(['read_and_close' => true]) |
Stays. Releasing the session lock is right; it just needed the timestamp kept up. |
| The tasks module | Never at fault. It is where the bug was visible, not where it lived. |
| The service layer | Also never at fault, though a reasonable first suspicion given tasks had been refactored onto it. |
Docker's php.ini
|
Its settings are PHP's own defaults. Turning the collector off to hide this would leave session files accumulating forever. |
Reproduced first, in a throwaway Docker install built from the current tree β because two people had already failed to reproduce it, and a fix for a bug you have not seen is a guess.
The mechanism was then established by measurement rather than argument:
| Test | Result |
|---|---|
| Session file timestamp after an API call | Unchanged |
| Same after a full page load | Updated |
Same API call with the pre-#389 session_start() shape |
Updated β pinning the regression |
Then the bug itself, end to end: age the session file 25 minutes, make 300 ordinary API calls β the file is deleted, the board redirects to the login screen, and saving a task returns "Not authenticated". With the fix, the same 300 calls leave the session refreshed and working.
The negative control is the one that mattered. A fix that simply stopped sessions expiring would pass every test above and be a security regression. So: a genuinely idle session, aged identically, while a different user works β it is still collected. Expiry is intact; it now measures activity instead of luck.
Two mistakes are worth recording, because both produced confident wrong answers:
- An apparent spontaneous logout during the investigation was the test tool's own cookie jar dropping a session cookie, not the bug. Checked before it was believed.
- The first attempt at the pre-#389 comparison gave a false negative, because opcache was serving the cached version of the file that had just been edited. The real answer only appeared after a reload. In a container, an edited PHP file is not necessarily the file being executed.
Update, and it stops. Nothing to configure, and no session settings to change.
If people on your installation have been quietly grumbling about being signed out during the day β particularly anyone who lives on the Tasks board β this was very likely why, and they probably never mentioned it because being logged out does not feel like a fault worth reporting.
You are still signed out when you are genuinely idle, on exactly the same schedule as before. That has not been relaxed.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)