Skip to content

🐛 [Bug]: server: protect field access with lock to avoid possible data race #3362

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
3 tasks done
ririli opened this issue Mar 19, 2025 · 15 comments
Open
3 tasks done
Milestone

Comments

@ririli
Copy link

ririli commented Mar 19, 2025

Bug Description

Fix Data Race Risk in fiber/app.go
Description:
In the fiber/app.go file, there is a potential data race risk related to the app.server field. Specifically:
The app.server field is read/written 21 times.20 of these accesses are properly synchronized using app.mutex.RLock() or Lock().
1 read operation (in the Server() function at line 882) is not locked, which may cause a data race when Server() and other functions like init() are called concurrently.
Suggested Fix:
Use app.mutex.RLock() or Lock() consistently to protect all accesses to app.server.
And store the result in a local variable to minimize lock contention.
Could you please help confirm this bug?

How to Reproduce

Steps to reproduce the behavior:

Expected Behavior

Data Race

Fiber Version

v2.42.0

Code Snippet (optional)

Checklist:

  • I agree to follow Fiber's Code of Conduct.
  • I have checked for existing issues that describe my problem prior to opening this one.
  • I understand that improperly formatted bug reports may be closed without explanation.
Copy link

welcome bot commented Mar 19, 2025

Thanks for opening your first issue here! 🎉 Be sure to follow the issue template! If you need help or want to chat with us, join us on Discord https://gofiber.io/discord

@ReneWerner87
Copy link
Member

hi can you please give us more information and a test example

there is nothing in line 882

fiber/app.go

Line 882 in 42d921d

}

data races usually occur when you execute functions in parallel in threads, since you describe the server start here and this should not be executed in parallel, I am curious about the usecase

@ririli
Copy link
Author

ririli commented Mar 19, 2025

Hi, link to code app.go (v2.42.0):https://github.com/gofiber/fiber/blob/v2.42.0/app.go#L882
Here in line 882 you can see Server(), I found through static analysis that the server field is not mutex protected only here in line 882 in that code, while all others are mutex protected. This could have potential data race when Server() is called in parallel with other functions like init().

@gaby
Copy link
Member

gaby commented Mar 19, 2025

What software you used for static analysis?

@ReneWerner87
Copy link
Member

ReneWerner87 commented Mar 19, 2025

does it make sense to include mutext there if no action is executed? mutext would have to be done outside if you want to make sure that no one customizes app.server

@coderabbitai how do other frameworks do this? what do you think as an independent AI?

Copy link
Contributor

coderabbitai bot commented Mar 19, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check out the specific version mentioned in the issue
git checkout v2.42.0

# Look at the Server() function in app.go
echo "### Server() function:"
grep -n "func.*Server" app.go -A 5

# Look at all occurrences of app.server in the file
echo -e "\n### All app.server usages:"
grep -n "app.server" app.go

# Check mutex usage with server
echo -e "\n### Mutex usage with server:"
grep -n "mutex.*Lock.*server\|server.*mutex.*Lock" app.go -A 3

Length of output: 2074


🏁 Script executed:

#!/bin/bash
# Fetch the tags and check the correct version
git fetch --tags
git checkout v2.42.0

# First look at the Server() function in app.go 
echo "### Server() function and surrounding code:"
grep -n -A 5 -B 5 "func.*Server" app.go | grep -v "^\-\-$"

# Check all mutex usage around app.server
echo -e "\n### Mutex usage when accessing app.server:"
grep -n -A 5 -B 5 "app\.mutex.*Lock.*" app.go | grep -n "app\.server"
grep -n -A 5 -B 5 "app\.mutex.*RLock.*" app.go | grep -n "app\.server"

# Check how other operations access app.server
echo -e "\n### Other frameworks - let's check Echo as an example:"
git clone https://github.com/labstack/echo.git /tmp/echo
cd /tmp/echo
grep -r "mutex" --include="*.go" .

Length of output: 8314


@ReneWerner87 I've analyzed the code in question and can provide some insight about mutex protection for the Server() function.

Looking at the v2.42.0 code:

// Server returns the underlying fasthttp server
func (app *App) Server() *fasthttp.Server {
	return app.server
}

From a concurrency standpoint, it does make sense to protect this read with a mutex for several reasons:

  1. Consistency principle: If 20 other places protect access to app.server with a mutex as mentioned in the issue, this suggests a design decision that the field should always be accessed under mutex protection.

  2. Race condition risks: Even though this is a "read-only" operation, it can still be involved in a race condition if another goroutine is modifying app.server at the same time (perhaps during initialization or shutdown).

  3. Visibility guarantees: In Go's memory model, a mutex not only prevents concurrent access but also ensures memory visibility across goroutines.

In other web frameworks like Echo, similar patterns are used where mutex protection is applied consistently. The general best practice is that if a field is accessed concurrently and protected by a mutex in some places, it should be protected in all places for consistency and correctness.

A simple fix would be:

func (app *App) Server() *fasthttp.Server {
	app.mutex.RLock()
	defer app.mutex.RUnlock()
	return app.server
}

This uses a read lock which allows concurrent reads but prevents reads during writes, providing thread safety with minimal performance impact.

Copy link
Contributor

coderabbitai bot commented Mar 19, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the Server() method and surrounding code from app.go (v2.42.0)
curl -s https://raw.githubusercontent.com/gofiber/fiber/v2.42.0/app.go | grep -n -A10 -B2 "func (app \*App) Server()"

# Check other places where app.server is accessed with mutex locks
curl -s https://raw.githubusercontent.com/gofiber/fiber/v2.42.0/app.go | grep -n "app.server" | grep -v "func (app \*App) Server()"

# Check other places where mutex locks are used when accessing app.server
curl -s https://raw.githubusercontent.com/gofiber/fiber/v2.42.0/app.go | grep -n -A2 -B2 "app.mutex.R?Lock()" | grep -A2 -B2 "app.server"

Length of output: 2128


🏁 Script executed:

#!/bin/bash
# Check for mutex locks around app.server access
echo "Checking for mutex locks around app.server access:"
curl -s https://raw.githubusercontent.com/gofiber/fiber/v2.42.0/app.go | grep -A5 -B5 "app.mutex" | grep -B10 -A10 "app.server"

# Get the entire ShutdownWithContext method to see mutex usage
echo -e "\nShutdownWithContext method:"
curl -s https://raw.githubusercontent.com/gofiber/fiber/v2.42.0/app.go | grep -A20 -B2 "func (app \*App) ShutdownWithContext"

# Look at other Go web frameworks for comparison (Echo)
echo -e "\nChecking Echo framework's server access patterns:"
curl -s https://raw.githubusercontent.com/labstack/echo/master/echo.go | grep -A5 -B5 "Server"

Length of output: 8102


🏁 Script executed:

#!/bin/bash
# Get the entire Server() method from app.go in Fiber (v2.42.0)
echo "Server() method in Fiber:"
curl -s https://raw.githubusercontent.com/gofiber/fiber/v2.42.0/app.go | grep -A3 -B2 "func (app \*App) Server()"

# Check how the server is initialized and where it's potentially modified
echo -e "\nServer initialization in Fiber:"
curl -s https://raw.githubusercontent.com/gofiber/fiber/v2.42.0/app.go | grep -n -A5 "app.server = "

# Check for usage examples of Server() method
echo -e "\nLooking for usage of Server() method:"
curl -s https://api.github.com/search/code?q=Server+repo:gofiber/fiber+path:test | jq '.items[].path' | xargs -I{} echo "https://github.com/gofiber/fiber/blob/master/{}"

Length of output: 957


@ReneWerner87 I've examined how Fiber and other frameworks handle server access. Here's my analysis:

Looking at the code:

  1. The Server() method simply returns app.server without any locking:
func (app *App) Server() *fasthttp.Server {
    return app.server
}
  1. All other accesses to app.server are protected with mutex locks, including:
    • In shutdownWithContext: app.mutex.Lock() before checking and using app.server
    • During initialization: app.server = &fasthttp.Server{...} is wrapped with mutex locks

From a concurrency perspective, adding a read lock to the Server() method would be a good practice for several reasons:

  1. Consistency: All other access points to app.server are protected with mutex locks. Being consistent avoids subtle bugs.

  2. Race condition protection: If one goroutine is initializing or modifying app.server while another calls Server(), there's a potential race condition.

  3. Standard Go pattern: It's common practice in Go to protect all shared state access with appropriate locks, even for read-only operations.

Similar frameworks like Echo also use mutex protection around server operations, though their approach is different (using a startupMutex that's locked during operations that modify the server).

I'd recommend adding a read lock to the Server() method:

func (app *App) Server() *fasthttp.Server {
    app.mutex.RLock()
    defer app.mutex.RUnlock()
    return app.server
}

This ensures thread safety with minimal performance impact since read locks allow concurrent reads.

@ReneWerner87
Copy link
Member

ok, then lets do this

@ReneWerner87 ReneWerner87 added this to v3 Mar 19, 2025
@ReneWerner87 ReneWerner87 added this to the v3 milestone Mar 19, 2025
@gaby
Copy link
Member

gaby commented Mar 19, 2025

Doesnt that mean that Hooks() also needs this? Nevermind :-)

@ririli
Copy link
Author

ririli commented Mar 19, 2025

What software you used for static analysis?

Hello, it's our self-developed software based on the official go language tools package.

@ririli
Copy link
Author

ririli commented Apr 6, 2025

ok, then lets do this

Hi, excuse me, now how do you feel about this, do you think it needs a locking operation on it?

@gaby gaby moved this to Todo in v3 Apr 25, 2025
@gaby
Copy link
Member

gaby commented Apr 25, 2025

@ririli It makes sense to add it.

@gaby
Copy link
Member

gaby commented Apr 25, 2025

@coderabbitai Is the RLock needed in any other method of app.go?

Copy link
Contributor

coderabbitai bot commented Apr 25, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Clone the repository and check out the specific version
git checkout v2.42.0

# First, let's look at the App struct definition to understand its fields
echo "### App struct definition:"
grep -n -A 30 "type App struct" app.go

# Now let's find all getter methods that might return fields directly
echo -e "\n### Potential getter methods in app.go:"
grep -n "func (app \*App)" app.go | grep "return app\." -A 1

# Let's look at where the mutex is used to protect field access
echo -e "\n### Methods that use mutex protection:"
grep -n -A 2 "app\.mutex\.R?Lock" app.go

# Let's specifically look at the Server() method for reference
echo -e "\n### Server() method:"
grep -n -A 4 "func (app \*App) Server" app.go

Length of output: 1705


🏁 Script executed:

#!/bin/bash
# Let's look more closely at the App struct to find the mutex field
echo "### Looking for mutex field in App struct:"
grep -n -A 50 "type App struct" app.go | grep mutex

# Let's find all methods that return fields of the App struct directly
echo -e "\n### Methods that return App fields directly:"
grep -n "func (app \*App)" app.go -A 3 | grep -B 1 "return app\."

# Let's look at where mutex is used
echo -e "\n### All mutex usage in app.go:"
grep -n "app\.mutex" app.go -A 2

# Let's look at the Hooks() method specifically since it was mentioned
echo -e "\n### Hooks() method:"
grep -n -A 5 "func (app \*App) Hooks" app.go

Length of output: 3512


🏁 Script executed:

#!/bin/bash
# Let's check all places where fields returned by getter methods are modified
echo "### Where app.hooks is modified:"
grep -n "app\.hooks =" app.go -A 2 -B 2

echo -e "\n### Where app.state is modified:"
grep -n "app\.state =" app.go -A 2 -B 2

echo -e "\n### Where app.server is modified:"
grep -n "app\.server =" app.go -A 2 -B 2

echo -e "\n### Where app.stack is modified:"
grep -n "app\.stack =" app.go -A 2 -B 2

echo -e "\n### Where app.handlersCount is modified:"
grep -n "app\.handlersCount =" app.go -A 2 -B 2

# Let's also look at the initial setup of these fields in New()
echo -e "\n### Initialization in New():"
grep -n -A 50 "func New(" app.go | grep -B 5 -A 5 "app := &App{"

Length of output: 1619


@gaby Yes, based on my analysis of the app.go file, RLock is needed in several other methods that directly return fields from the App struct. These methods could face the same race condition issues as Server():

  1. Hooks() method (line 968):
func (app *App) Hooks() *Hooks {
    return app.hooks
}
  1. State() method (line 973):
func (app *App) State() *State {
    return app.state
}
  1. Stack() method (line 900):
func (app *App) Stack() [][]*Route {
    return app.stack
}
  1. HandlersCount() method (line 905):
func (app *App) HandlersCount() uint32 {
    return app.handlersCount
}
  1. Config() method (line 884):
func (app *App) Config() Config {
    return app.config
}

All these methods directly return fields that could be modified elsewhere in the codebase. For consistency and to prevent potential race conditions, each should be protected with a read lock following the same pattern proposed for Server():

func (app *App) MethodName() ReturnType {
    app.mutex.RLock()
    defer app.mutex.RUnlock()
    return app.fieldName
}

This approach ensures thread safety while allowing concurrent reads, which is important for performance in a web framework.

@gaby
Copy link
Member

gaby commented Apr 25, 2025

Adding a lock on these functions causes unit-tests to never finish since the locks are fighting with each other.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Todo
Development

Successfully merging a pull request may close this issue.

3 participants